bug 767: uniform role format
[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
27
28 __author__ = "Eduardo Sousa <esousa@whitestack.com>"
29 __date__ = "$27-jul-2018 23:59:59$"
30
31 from authconn import Authconn, AuthException, AuthconnOperationException, AuthconnNotFoundException, \
32 AuthconnConflictException
33
34 import logging
35 import requests
36 import time
37 from keystoneauth1 import session
38 from keystoneauth1.identity import v3
39 from keystoneauth1.exceptions.base import ClientException
40 from keystoneauth1.exceptions.http import Conflict
41 from keystoneclient.v3 import client
42 from http import HTTPStatus
43 from validation import is_valid_uuid
44
45
46 class AuthconnKeystone(Authconn):
47 def __init__(self, config):
48 Authconn.__init__(self, config)
49
50 self.logger = logging.getLogger("nbi.authenticator.keystone")
51
52 self.auth_url = "http://{0}:{1}/v3".format(config.get("auth_url", "keystone"), config.get("auth_port", "5000"))
53 self.user_domain_name = config.get("user_domain_name", "default")
54 self.admin_project = config.get("service_project", "service")
55 self.admin_username = config.get("service_username", "nbi")
56 self.admin_password = config.get("service_password", "nbi")
57 self.project_domain_name = config.get("project_domain_name", "default")
58
59 # Waiting for Keystone to be up
60 available = None
61 counter = 300
62 while available is None:
63 time.sleep(1)
64 try:
65 result = requests.get(self.auth_url)
66 available = True if result.status_code == 200 else None
67 except Exception:
68 counter -= 1
69 if counter == 0:
70 raise AuthException("Keystone not available after 300s timeout")
71
72 self.auth = v3.Password(user_domain_name=self.user_domain_name,
73 username=self.admin_username,
74 password=self.admin_password,
75 project_domain_name=self.project_domain_name,
76 project_name=self.admin_project,
77 auth_url=self.auth_url)
78 self.sess = session.Session(auth=self.auth)
79 self.keystone = client.Client(session=self.sess)
80
81 def authenticate(self, user, password, project=None, token=None):
82 """
83 Authenticate a user using username/password or token, plus project
84 :param user: user: name, id or None
85 :param password: password or None
86 :param project: name, id, or None. If None first found project will be used to get an scope token
87 :param token: previous token to obtain authorization
88 :return: the scoped token info or raises an exception. The token is a dictionary with:
89 _id: token string id,
90 username: username,
91 project_id: scoped_token project_id,
92 project_name: scoped_token project_name,
93 expires: epoch time when it expires,
94
95 """
96 try:
97 username = None
98 user_id = None
99 project_id = None
100 project_name = None
101
102 if user:
103 if is_valid_uuid(user):
104 user_id = user
105 else:
106 username = user
107
108 # get an unscoped token firstly
109 unscoped_token = self.keystone.get_raw_token_from_identity_service(
110 auth_url=self.auth_url,
111 user_id=user_id,
112 username=username,
113 password=password,
114 user_domain_name=self.user_domain_name,
115 project_domain_name=self.project_domain_name)
116 elif token:
117 unscoped_token = self.keystone.tokens.validate(token=token)
118 else:
119 raise AuthException("Provide credentials: username/password or Authorization Bearer token",
120 http_code=HTTPStatus.UNAUTHORIZED)
121
122 if not project:
123 # get first project for the user
124 project_list = self.keystone.projects.list(user=unscoped_token["user"]["id"])
125 if not project_list:
126 raise AuthException("The user {} has not any project and cannot be used for authentication".
127 format(user), http_code=HTTPStatus.UNAUTHORIZED)
128 project_id = project_list[0].id
129 else:
130 if is_valid_uuid(project):
131 project_id = project
132 else:
133 project_name = project
134
135 scoped_token = self.keystone.get_raw_token_from_identity_service(
136 auth_url=self.auth_url,
137 project_name=project_name,
138 project_id=project_id,
139 user_domain_name=self.user_domain_name,
140 project_domain_name=self.project_domain_name,
141 token=unscoped_token["auth_token"])
142
143 auth_token = {
144 "_id": scoped_token.auth_token,
145 "username": scoped_token.username,
146 "project_id": scoped_token.project_id,
147 "project_name": scoped_token.project_name,
148 "expires": scoped_token.expires.timestamp(),
149 }
150
151 return auth_token
152 except ClientException as e:
153 self.logger.exception("Error during user authentication using keystone. Method: basic: {}".format(e))
154 raise AuthException("Error during user authentication using Keystone: {}".format(e),
155 http_code=HTTPStatus.UNAUTHORIZED)
156
157 # def authenticate_with_token(self, token, project=None):
158 # """
159 # Authenticate a user using a token. Can be used to revalidate the token
160 # or to get a scoped token.
161 #
162 # :param token: a valid token.
163 # :param project: (optional) project for a scoped token.
164 # :return: return a revalidated token, scoped if a project was passed or
165 # the previous token was already scoped.
166 # """
167 # try:
168 # token_info = self.keystone.tokens.validate(token=token)
169 # projects = self.keystone.projects.list(user=token_info["user"]["id"])
170 # project_names = [project.name for project in projects]
171 #
172 # new_token = self.keystone.get_raw_token_from_identity_service(
173 # auth_url=self.auth_url,
174 # token=token,
175 # project_name=project,
176 # project_id=None,
177 # user_domain_name=self.user_domain_name,
178 # project_domain_name=self.project_domain_name)
179 #
180 # return new_token["auth_token"], project_names
181 # except ClientException as e:
182 # self.logger.exception("Error during user authentication using keystone. Method: bearer: {}".format(e))
183 # raise AuthException("Error during user authentication using Keystone: {}".format(e),
184 # http_code=HTTPStatus.UNAUTHORIZED)
185
186 def validate_token(self, token):
187 """
188 Check if the token is valid.
189
190 :param token: token to validate
191 :return: dictionary with information associated with the token. If the
192 token is not valid, returns None.
193 """
194 if not token:
195 return
196
197 try:
198 token_info = self.keystone.tokens.validate(token=token)
199
200 return token_info
201 except ClientException as e:
202 self.logger.exception("Error during token validation using keystone: {}".format(e))
203 raise AuthException("Error during token validation using Keystone: {}".format(e),
204 http_code=HTTPStatus.UNAUTHORIZED)
205
206 def revoke_token(self, token):
207 """
208 Invalidate a token.
209
210 :param token: token to be revoked
211 """
212 try:
213 self.logger.info("Revoking token: " + token)
214 self.keystone.tokens.revoke_token(token=token)
215
216 return True
217 except ClientException as e:
218 self.logger.exception("Error during token revocation using keystone: {}".format(e))
219 raise AuthException("Error during token revocation using Keystone: {}".format(e),
220 http_code=HTTPStatus.UNAUTHORIZED)
221
222 def get_user_project_list(self, token):
223 """
224 Get all the projects associated with a user.
225
226 :param token: valid token
227 :return: list of projects
228 """
229 try:
230 token_info = self.keystone.tokens.validate(token=token)
231 projects = self.keystone.projects.list(user=token_info["user"]["id"])
232 project_names = [project.name for project in projects]
233
234 return project_names
235 except ClientException as e:
236 self.logger.exception("Error during user project listing using keystone: {}".format(e))
237 raise AuthException("Error during user project listing using Keystone: {}".format(e),
238 http_code=HTTPStatus.UNAUTHORIZED)
239
240 def get_user_role_list(self, token):
241 """
242 Get role list for a scoped project.
243
244 :param token: scoped token.
245 :return: returns the list of roles for the user in that project. If
246 the token is unscoped it returns None.
247 """
248 try:
249 token_info = self.keystone.tokens.validate(token=token)
250 roles_info = self.keystone.roles.list(user=token_info["user"]["id"], project=token_info["project"]["id"])
251
252 roles = [role.name for role in roles_info]
253
254 return roles
255 except ClientException as e:
256 self.logger.exception("Error during user role listing using keystone: {}".format(e))
257 raise AuthException("Error during user role listing using Keystone: {}".format(e),
258 http_code=HTTPStatus.UNAUTHORIZED)
259
260 def create_user(self, user, password):
261 """
262 Create a user.
263
264 :param user: username.
265 :param password: password.
266 :raises AuthconnOperationException: if user creation failed.
267 :return: returns the id of the user in keystone.
268 """
269 try:
270 new_user = self.keystone.users.create(user, password=password, domain=self.user_domain_name)
271 return {"username": new_user.name, "_id": new_user.id}
272 except Conflict as e:
273 # self.logger.exception("Error during user creation using keystone: {}".format(e))
274 raise AuthconnOperationException(e, http_code=HTTPStatus.CONFLICT)
275 except ClientException as e:
276 self.logger.exception("Error during user creation using keystone: {}".format(e))
277 raise AuthconnOperationException("Error during user creation using Keystone: {}".format(e))
278
279 def update_user(self, user, new_name=None, new_password=None):
280 """
281 Change the user name and/or password.
282
283 :param user: username or user_id
284 :param new_name: new name
285 :param new_password: new password.
286 :raises AuthconnOperationException: if change failed.
287 """
288 try:
289 if is_valid_uuid(user):
290 user_id = user
291 else:
292 user_obj_list = self.keystone.users.list(name=user)
293 if not user_obj_list:
294 raise AuthconnNotFoundException("User '{}' not found".format(user))
295 user_id = user_obj_list[0].id
296
297 self.keystone.users.update(user_id, password=new_password, name=new_name)
298 except ClientException as e:
299 self.logger.exception("Error during user password/name update using keystone: {}".format(e))
300 raise AuthconnOperationException("Error during user password/name update using Keystone: {}".format(e))
301
302 def delete_user(self, user_id):
303 """
304 Delete user.
305
306 :param user_id: user identifier.
307 :raises AuthconnOperationException: if user deletion failed.
308 """
309 try:
310 # users = self.keystone.users.list()
311 # user_obj = [user for user in users if user.id == user_id][0]
312 # result, _ = self.keystone.users.delete(user_obj)
313
314 result, detail = self.keystone.users.delete(user_id)
315 if result.status_code != 204:
316 raise ClientException("error {} {}".format(result.status_code, detail))
317
318 return True
319 except ClientException as e:
320 self.logger.exception("Error during user deletion using keystone: {}".format(e))
321 raise AuthconnOperationException("Error during user deletion using Keystone: {}".format(e))
322
323 def get_user_list(self, filter_q=None):
324 """
325 Get user list.
326
327 :param filter_q: dictionary to filter user list by name (username is also admited) and/or _id
328 :return: returns a list of users.
329 """
330 try:
331 filter_name = None
332 if filter_q:
333 filter_name = filter_q.get("name") or filter_q.get("username")
334 users = self.keystone.users.list(name=filter_name)
335 users = [{
336 "username": user.name,
337 "_id": user.id,
338 "id": user.id
339 } for user in users if user.name != self.admin_username]
340
341 if filter_q and filter_q.get("_id"):
342 users = [user for user in users if filter_q["_id"] == user["_id"]]
343
344 for user in users:
345 projects = self.keystone.projects.list(user=user["_id"])
346 projects = [{
347 "name": project.name,
348 "_id": project.id,
349 "id": project.id
350 } for project in projects]
351
352 for project in projects:
353 roles = self.keystone.roles.list(user=user["_id"], project=project["_id"])
354 roles = [{
355 "name": role.name,
356 "_id": role.id,
357 "id": role.id
358 } for role in roles]
359 project["roles"] = roles
360
361 user["projects"] = projects
362
363 return users
364 except ClientException as e:
365 self.logger.exception("Error during user listing using keystone: {}".format(e))
366 raise AuthconnOperationException("Error during user listing using Keystone: {}".format(e))
367
368 def get_role_list(self, filter_q=None):
369 """
370 Get role list.
371
372 :param filter_q: dictionary to filter role list by _id and/or name.
373 :return: returns the list of roles.
374 """
375 try:
376 filter_name = None
377 if filter_q:
378 filter_name = filter_q.get("name")
379 roles_list = self.keystone.roles.list(name=filter_name)
380
381 roles = [{
382 "name": role.name,
383 "_id": role.id
384 } for role in roles_list if role.name != "service"]
385
386 if filter_q and filter_q.get("_id"):
387 roles = [role for role in roles if filter_q["_id"] == role["_id"]]
388
389 return roles
390 except ClientException as e:
391 self.logger.exception("Error during user role listing using keystone: {}".format(e))
392 raise AuthException("Error during user role listing using Keystone: {}".format(e),
393 http_code=HTTPStatus.UNAUTHORIZED)
394
395 def create_role(self, role):
396 """
397 Create a role.
398
399 :param role: role name.
400 :raises AuthconnOperationException: if role creation failed.
401 """
402 try:
403 result = self.keystone.roles.create(role)
404 return result.id
405 except Conflict as ex:
406 raise AuthconnConflictException(str(ex))
407 except ClientException as e:
408 self.logger.exception("Error during role creation using keystone: {}".format(e))
409 raise AuthconnOperationException("Error during role creation using Keystone: {}".format(e))
410
411 def delete_role(self, role_id):
412 """
413 Delete a role.
414
415 :param role_id: role identifier.
416 :raises AuthconnOperationException: if role deletion failed.
417 """
418 try:
419 result, detail = self.keystone.roles.delete(role_id)
420
421 if result.status_code != 204:
422 raise ClientException("error {} {}".format(result.status_code, detail))
423
424 return True
425 except ClientException as e:
426 self.logger.exception("Error during role deletion using keystone: {}".format(e))
427 raise AuthconnOperationException("Error during role deletion using Keystone: {}".format(e))
428
429 def update_role(self, role, new_name):
430 """
431 Change the name of a role
432 :param role: role name or id to be changed
433 :param new_name: new name
434 :return: None
435 """
436 try:
437 if is_valid_uuid(role):
438 role_id = role
439 else:
440 role_obj_list = self.keystone.roles.list(name=role)
441 if not role_obj_list:
442 raise AuthconnNotFoundException("Role '{}' not found".format(role))
443 role_id = role_obj_list[0].id
444 self.keystone.roles.update(role_id, name=new_name)
445 except ClientException as e:
446 # self.logger.exception("Error during role update using keystone: {}".format(e))
447 raise AuthconnOperationException("Error during role updating using Keystone: {}".format(e))
448
449 def get_project_list(self, filter_q=None):
450 """
451 Get all the projects.
452
453 :param filter_q: dictionary to filter project list.
454 :return: list of projects
455 """
456 try:
457 filter_name = None
458 if filter_q:
459 filter_name = filter_q.get("name")
460 projects = self.keystone.projects.list(name=filter_name)
461
462 projects = [{
463 "name": project.name,
464 "_id": project.id
465 } for project in projects]
466
467 if filter_q and filter_q.get("_id"):
468 projects = [project for project in projects
469 if filter_q["_id"] == project["_id"]]
470
471 return projects
472 except ClientException as e:
473 self.logger.exception("Error during user project listing using keystone: {}".format(e))
474 raise AuthException("Error during user project listing using Keystone: {}".format(e),
475 http_code=HTTPStatus.UNAUTHORIZED)
476
477 def create_project(self, project):
478 """
479 Create a project.
480
481 :param project: project name.
482 :return: the internal id of the created project
483 :raises AuthconnOperationException: if project creation failed.
484 """
485 try:
486 result = self.keystone.projects.create(project, self.project_domain_name)
487 return result.id
488 except ClientException as e:
489 self.logger.exception("Error during project creation using keystone: {}".format(e))
490 raise AuthconnOperationException("Error during project creation using Keystone: {}".format(e))
491
492 def delete_project(self, project_id):
493 """
494 Delete a project.
495
496 :param project_id: project identifier.
497 :raises AuthconnOperationException: if project deletion failed.
498 """
499 try:
500 # projects = self.keystone.projects.list()
501 # project_obj = [project for project in projects if project.id == project_id][0]
502 # result, _ = self.keystone.projects.delete(project_obj)
503
504 result, detail = self.keystone.projects.delete(project_id)
505 if result.status_code != 204:
506 raise ClientException("error {} {}".format(result.status_code, detail))
507
508 return True
509 except ClientException as e:
510 self.logger.exception("Error during project deletion using keystone: {}".format(e))
511 raise AuthconnOperationException("Error during project deletion using Keystone: {}".format(e))
512
513 def update_project(self, project_id, new_name):
514 """
515 Change the name of a project
516 :param project_id: project to be changed
517 :param new_name: new name
518 :return: None
519 """
520 try:
521 self.keystone.projects.update(project_id, name=new_name)
522 except ClientException as e:
523 self.logger.exception("Error during project update using keystone: {}".format(e))
524 raise AuthconnOperationException("Error during project deletion using Keystone: {}".format(e))
525
526 def assign_role_to_user(self, user, project, role):
527 """
528 Assigning a role to a user in a project.
529
530 :param user: username.
531 :param project: project name.
532 :param role: role name.
533 :raises AuthconnOperationException: if role assignment failed.
534 """
535 try:
536 if is_valid_uuid(user):
537 user_obj = self.keystone.users.get(user)
538 else:
539 user_obj_list = self.keystone.users.list(name=user)
540 if not user_obj_list:
541 raise AuthconnNotFoundException("User '{}' not found".format(user))
542 user_obj = user_obj_list[0]
543
544 if is_valid_uuid(project):
545 project_obj = self.keystone.projects.get(project)
546 else:
547 project_obj_list = self.keystone.projects.list(name=project)
548 if not project_obj_list:
549 raise AuthconnNotFoundException("Project '{}' not found".format(project))
550 project_obj = project_obj_list[0]
551
552 if is_valid_uuid(role):
553 role_obj = self.keystone.roles.get(role)
554 else:
555 role_obj_list = self.keystone.roles.list(name=role)
556 if not role_obj_list:
557 raise AuthconnNotFoundException("Role '{}' not found".format(role))
558 role_obj = role_obj_list[0]
559
560 self.keystone.roles.grant(role_obj, user=user_obj, project=project_obj)
561 except ClientException as e:
562 self.logger.exception("Error during user role assignment using keystone: {}".format(e))
563 raise AuthconnOperationException("Error during role '{}' assignment to user '{}' and project '{}' using "
564 "Keystone: {}".format(role, user, project, e))
565
566 def remove_role_from_user(self, user, project, role):
567 """
568 Remove a role from a user in a project.
569
570 :param user: username.
571 :param project: project name or id.
572 :param role: role name or id.
573
574 :raises AuthconnOperationException: if role assignment revocation failed.
575 """
576 try:
577 if is_valid_uuid(user):
578 user_obj = self.keystone.users.get(user)
579 else:
580 user_obj_list = self.keystone.users.list(name=user)
581 if not user_obj_list:
582 raise AuthconnNotFoundException("User '{}' not found".format(user))
583 user_obj = user_obj_list[0]
584
585 if is_valid_uuid(project):
586 project_obj = self.keystone.projects.get(project)
587 else:
588 project_obj_list = self.keystone.projects.list(name=project)
589 if not project_obj_list:
590 raise AuthconnNotFoundException("Project '{}' not found".format(project))
591 project_obj = project_obj_list[0]
592
593 if is_valid_uuid(role):
594 role_obj = self.keystone.roles.get(role)
595 else:
596 role_obj_list = self.keystone.roles.list(name=role)
597 if not role_obj_list:
598 raise AuthconnNotFoundException("Role '{}' not found".format(role))
599 role_obj = role_obj_list[0]
600
601 self.keystone.roles.revoke(role_obj, user=user_obj, project=project_obj)
602 except ClientException as e:
603 self.logger.exception("Error during user role revocation using keystone: {}".format(e))
604 raise AuthconnOperationException("Error during role '{}' revocation to user '{}' and project '{}' using "
605 "Keystone: {}".format(role, user, project, e))