7f592700c581d2a8ae6726f689c3dfaa06192d25
[osm/NBI.git] / osm_nbi / authconn_keystone.py
1 # -*- coding: utf-8 -*-
2
3 # Copyright 2018 Whitestack, LLC
4 #
5 # Licensed under the Apache License, Version 2.0 (the "License"); you may
6 # not use this file except in compliance with the License. You may obtain
7 # a copy of the License at
8 #
9 # http://www.apache.org/licenses/LICENSE-2.0
10 #
11 # Unless required by applicable law or agreed to in writing, software
12 # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
13 # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
14 # License for the specific language governing permissions and limitations
15 # under the License.
16 #
17 # For those usages not covered by the Apache License, Version 2.0 please
18 # contact: esousa@whitestack.com or glavado@whitestack.com
19 ##
20
21 """
22 AuthconnKeystone implements implements the connector for
23 Openstack Keystone and leverages the RBAC model, to bring
24 it for OSM.
25 """
26 import time
27
28 __author__ = "Eduardo Sousa <esousa@whitestack.com>"
29 __date__ = "$27-jul-2018 23:59:59$"
30
31 from authconn import Authconn, AuthException, AuthconnOperationException
32
33 import logging
34 import requests
35 from keystoneauth1 import session
36 from keystoneauth1.identity import v3
37 from keystoneauth1.exceptions.base import ClientException
38 from keystoneauth1.exceptions.http import Conflict
39 from keystoneclient.v3 import client
40 from http import HTTPStatus
41
42
43 class AuthconnKeystone(Authconn):
44 def __init__(self, config):
45 Authconn.__init__(self, config)
46
47 self.logger = logging.getLogger("nbi.authenticator.keystone")
48
49 self.auth_url = "http://{0}:{1}/v3".format(config.get("auth_url", "keystone"), config.get("auth_port", "5000"))
50 self.user_domain_name = config.get("user_domain_name", "default")
51 self.admin_project = config.get("service_project", "service")
52 self.admin_username = config.get("service_username", "nbi")
53 self.admin_password = config.get("service_password", "nbi")
54 self.project_domain_name = config.get("project_domain_name", "default")
55
56 # Waiting for Keystone to be up
57 available = None
58 counter = 300
59 while available is None:
60 time.sleep(1)
61 try:
62 result = requests.get(self.auth_url)
63 available = True if result.status_code == 200 else None
64 except Exception:
65 counter -= 1
66 if counter == 0:
67 raise AuthException("Keystone not available after 300s timeout")
68
69 self.auth = v3.Password(user_domain_name=self.user_domain_name,
70 username=self.admin_username,
71 password=self.admin_password,
72 project_domain_name=self.project_domain_name,
73 project_name=self.admin_project,
74 auth_url=self.auth_url)
75 self.sess = session.Session(auth=self.auth)
76 self.keystone = client.Client(session=self.sess)
77
78 def authenticate_with_user_password(self, user, password):
79 """
80 Authenticate a user using username and password.
81
82 :param user: username
83 :param password: password
84 :return: an unscoped token that grants access to project list
85 """
86 try:
87 user_id = list(filter(lambda x: x.name == user, self.keystone.users.list()))[0].id
88 project_names = [project.name for project in self.keystone.projects.list(user=user_id)]
89
90 token = self.keystone.get_raw_token_from_identity_service(
91 auth_url=self.auth_url,
92 username=user,
93 password=password,
94 user_domain_name=self.user_domain_name,
95 project_domain_name=self.project_domain_name)
96
97 return token["auth_token"], project_names
98 except ClientException:
99 self.logger.exception("Error during user authentication using keystone. Method: basic")
100 raise AuthException("Error during user authentication using Keystone", http_code=HTTPStatus.UNAUTHORIZED)
101
102 def authenticate_with_token(self, token, project=None):
103 """
104 Authenticate a user using a token. Can be used to revalidate the token
105 or to get a scoped token.
106
107 :param token: a valid token.
108 :param project: (optional) project for a scoped token.
109 :return: return a revalidated token, scoped if a project was passed or
110 the previous token was already scoped.
111 """
112 try:
113 token_info = self.keystone.tokens.validate(token=token)
114 projects = self.keystone.projects.list(user=token_info["user"]["id"])
115 project_names = [project.name for project in projects]
116
117 new_token = self.keystone.get_raw_token_from_identity_service(
118 auth_url=self.auth_url,
119 token=token,
120 project_name=project,
121 user_domain_name=self.user_domain_name,
122 project_domain_name=self.project_domain_name)
123
124 return new_token["auth_token"], project_names
125 except ClientException:
126 self.logger.exception("Error during user authentication using keystone. Method: bearer")
127 raise AuthException("Error during user authentication using Keystone", http_code=HTTPStatus.UNAUTHORIZED)
128
129 def validate_token(self, token):
130 """
131 Check if the token is valid.
132
133 :param token: token to validate
134 :return: dictionary with information associated with the token. If the
135 token is not valid, returns None.
136 """
137 if not token:
138 return
139
140 try:
141 token_info = self.keystone.tokens.validate(token=token)
142
143 return token_info
144 except ClientException:
145 self.logger.exception("Error during token validation using keystone")
146 raise AuthException("Error during token validation using Keystone", http_code=HTTPStatus.UNAUTHORIZED)
147
148 def revoke_token(self, token):
149 """
150 Invalidate a token.
151
152 :param token: token to be revoked
153 """
154 try:
155 self.logger.info("Revoking token: " + token)
156 self.keystone.tokens.revoke_token(token=token)
157
158 return True
159 except ClientException:
160 self.logger.exception("Error during token revocation using keystone")
161 raise AuthException("Error during token revocation using Keystone", http_code=HTTPStatus.UNAUTHORIZED)
162
163 def get_user_project_list(self, token):
164 """
165 Get all the projects associated with a user.
166
167 :param token: valid token
168 :return: list of projects
169 """
170 try:
171 token_info = self.keystone.tokens.validate(token=token)
172 projects = self.keystone.projects.list(user=token_info["user"]["id"])
173 project_names = [project.name for project in projects]
174
175 return project_names
176 except ClientException:
177 self.logger.exception("Error during user project listing using keystone")
178 raise AuthException("Error during user project listing using Keystone", http_code=HTTPStatus.UNAUTHORIZED)
179
180 def get_user_role_list(self, token):
181 """
182 Get role list for a scoped project.
183
184 :param token: scoped token.
185 :return: returns the list of roles for the user in that project. If
186 the token is unscoped it returns None.
187 """
188 try:
189 token_info = self.keystone.tokens.validate(token=token)
190 roles_info = self.keystone.roles.list(user=token_info["user"]["id"], project=token_info["project"]["id"])
191
192 roles = [role.name for role in roles_info]
193
194 return roles
195 except ClientException:
196 self.logger.exception("Error during user role listing using keystone")
197 raise AuthException("Error during user role listing using Keystone", http_code=HTTPStatus.UNAUTHORIZED)
198
199 def create_user(self, user, password):
200 """
201 Create a user.
202
203 :param user: username.
204 :param password: password.
205 :raises AuthconnOperationException: if user creation failed.
206 :return: returns the id of the user in keystone.
207 """
208 try:
209 new_user = self.keystone.users.create(user, password=password, domain=self.user_domain_name)
210 return {"username": new_user.name, "_id": new_user.id}
211 except ClientException:
212 self.logger.exception("Error during user creation using keystone")
213 raise AuthconnOperationException("Error during user creation using Keystone")
214
215 def change_password(self, user, new_password):
216 """
217 Change the user password.
218
219 :param user: username.
220 :param new_password: new password.
221 :raises AuthconnOperationException: if user password change failed.
222 """
223 try:
224 user_obj = list(filter(lambda x: x.name == user, self.keystone.users.list()))[0]
225 self.keystone.users.update(user_obj, password=new_password)
226 except ClientException:
227 self.logger.exception("Error during user password update using keystone")
228 raise AuthconnOperationException("Error during user password update using Keystone")
229
230 def delete_user(self, user_id):
231 """
232 Delete user.
233
234 :param user_id: user identifier.
235 :raises AuthconnOperationException: if user deletion failed.
236 """
237 try:
238 users = self.keystone.users.list()
239 user_obj = [user for user in users if user.id == user_id][0]
240 result, _ = self.keystone.users.delete(user_obj)
241
242 if result.status_code != 204:
243 raise ClientException("User was not deleted")
244
245 return True
246 except ClientException:
247 self.logger.exception("Error during user deletion using keystone")
248 raise AuthconnOperationException("Error during user deletion using Keystone")
249
250 def get_user_list(self, filter_q={}):
251 """
252 Get user list.
253
254 :param filter_q: dictionary to filter user list.
255 :return: returns a list of users.
256 """
257 try:
258 users = self.keystone.users.list()
259 users = [{
260 "username": user.name,
261 "_id": user.id,
262 "id": user.id
263 } for user in users if user.name != self.admin_username]
264
265 allowed_fields = ["_id", "id", "username"]
266 for key in filter_q.keys():
267 if key not in allowed_fields:
268 continue
269
270 users = [user for user in users
271 if filter_q[key] == user[key]]
272
273 for user in users:
274 projects = self.keystone.projects.list(user=user["_id"])
275 projects = [{
276 "name": project.name,
277 "_id": project.id,
278 "id": project.id
279 } for project in projects]
280
281 for project in projects:
282 roles = self.keystone.roles.list(user=user["_id"], project=project["_id"])
283 roles = [{
284 "name": role.name,
285 "_id": role.id,
286 "id": role.id
287 } for role in roles]
288 project["roles"] = roles
289
290 user["projects"] = projects
291
292 return users
293 except ClientException:
294 self.logger.exception("Error during user listing using keystone")
295 raise AuthconnOperationException("Error during user listing using Keystone")
296
297 def get_role_list(self):
298 """
299 Get role list.
300
301 :return: returns the list of roles.
302 """
303 try:
304 roles_list = self.keystone.roles.list()
305
306 roles = [{
307 "name": role.name,
308 "_id": role.id
309 } for role in roles_list if role.name != "service"]
310
311 return roles
312 except ClientException:
313 self.logger.exception("Error during user role listing using keystone")
314 raise AuthException("Error during user role listing using Keystone", http_code=HTTPStatus.UNAUTHORIZED)
315
316 def create_role(self, role):
317 """
318 Create a role.
319
320 :param role: role name.
321 :raises AuthconnOperationException: if role creation failed.
322 """
323 try:
324 result = self.keystone.roles.create(role)
325 return {"name": result.name, "_id": result.id}
326 except Conflict as ex:
327 self.logger.info("Duplicate entry: %s", str(ex))
328 except ClientException:
329 self.logger.exception("Error during role creation using keystone")
330 raise AuthconnOperationException("Error during role creation using Keystone")
331
332 def delete_role(self, role_id):
333 """
334 Delete a role.
335
336 :param role_id: role identifier.
337 :raises AuthconnOperationException: if role deletion failed.
338 """
339 try:
340 roles = self.keystone.roles.list()
341 role_obj = [role for role in roles if role.id == role_id][0]
342 result, _ = self.keystone.roles.delete(role_obj)
343
344 if result.status_code != 204:
345 raise ClientException("Role was not deleted")
346
347 return True
348 except ClientException:
349 self.logger.exception("Error during role deletion using keystone")
350 raise AuthconnOperationException("Error during role deletion using Keystone")
351
352 def get_project_list(self, filter_q={}):
353 """
354 Get all the projects.
355
356 :param filter_q: dictionary to filter project list.
357 :return: list of projects
358 """
359 try:
360 projects = self.keystone.projects.list()
361 projects = [{
362 "name": project.name,
363 "_id": project.id
364 } for project in projects if project.name != self.admin_project]
365
366 allowed_fields = ["_id", "name"]
367 for key in filter_q.keys():
368 if key not in allowed_fields:
369 continue
370
371 projects = [project for project in projects
372 if filter_q[key] == project[key]]
373
374 return projects
375 except ClientException:
376 self.logger.exception("Error during user project listing using keystone")
377 raise AuthException("Error during user project listing using Keystone", http_code=HTTPStatus.UNAUTHORIZED)
378
379 def create_project(self, project):
380 """
381 Create a project.
382
383 :param project: project name.
384 :raises AuthconnOperationException: if project creation failed.
385 """
386 try:
387 result = self.keystone.projects.create(project, self.project_domain_name)
388 return {"name": result.name, "_id": result.id}
389 except ClientException:
390 self.logger.exception("Error during project creation using keystone")
391 raise AuthconnOperationException("Error during project creation using Keystone")
392
393 def delete_project(self, project_id):
394 """
395 Delete a project.
396
397 :param project_id: project identifier.
398 :raises AuthconnOperationException: if project deletion failed.
399 """
400 try:
401 projects = self.keystone.projects.list()
402 project_obj = [project for project in projects if project.id == project_id][0]
403 result, _ = self.keystone.projects.delete(project_obj)
404
405 if result.status_code != 204:
406 raise ClientException("Project was not deleted")
407
408 return True
409 except ClientException:
410 self.logger.exception("Error during project deletion using keystone")
411 raise AuthconnOperationException("Error during project deletion using Keystone")
412
413 def assign_role_to_user(self, user, project, role):
414 """
415 Assigning a role to a user in a project.
416
417 :param user: username.
418 :param project: project name.
419 :param role: role name.
420 :raises AuthconnOperationException: if role assignment failed.
421 """
422 try:
423 user_obj = list(filter(lambda x: x.name == user, self.keystone.users.list()))[0]
424 project_obj = list(filter(lambda x: x.name == project, self.keystone.projects.list()))[0]
425 role_obj = list(filter(lambda x: x.name == role, self.keystone.roles.list()))[0]
426
427 self.keystone.roles.grant(role_obj, user=user_obj, project=project_obj)
428 except ClientException:
429 self.logger.exception("Error during user role assignment using keystone")
430 raise AuthconnOperationException("Error during user role assignment using Keystone")
431
432 def remove_role_from_user(self, user, project, role):
433 """
434 Remove a role from a user in a project.
435
436 :param user: username.
437 :param project: project name.
438 :param role: role name.
439 :raises AuthconnOperationException: if role assignment revocation failed.
440 """
441 try:
442 user_obj = list(filter(lambda x: x.name == user, self.keystone.users.list()))[0]
443 project_obj = list(filter(lambda x: x.name == project, self.keystone.projects.list()))[0]
444 role_obj = list(filter(lambda x: x.name == role, self.keystone.roles.list()))[0]
445
446 self.keystone.roles.revoke(role_obj, user=user_obj, project=project_obj)
447 except ClientException:
448 self.logger.exception("Error during user role revocation using keystone")
449 raise AuthconnOperationException("Error during user role revocation using Keystone")