1 # -*- coding: utf-8 -*-
3 # Copyright 2018 Whitestack, LLC
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
9 # http://www.apache.org/licenses/LICENSE-2.0
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
17 # For those usages not covered by the Apache License, Version 2.0 please
18 # contact: esousa@whitestack.com or glavado@whitestack.com
22 AuthconnKeystone implements implements the connector for
23 Openstack Keystone and leverages the RBAC model, to bring
28 __author__
= "Eduardo Sousa <esousa@whitestack.com>, " \
29 "Pedro de la Cruz Ramos <pdelacruzramos@altran.com>"
30 __date__
= "$27-jul-2018 23:59:59$"
32 from osm_nbi
.authconn
import Authconn
, AuthException
, AuthconnOperationException
, AuthconnNotFoundException
, \
33 AuthconnConflictException
38 from keystoneauth1
import session
39 from keystoneauth1
.identity
import v3
40 from keystoneauth1
.exceptions
.base
import ClientException
41 from keystoneauth1
.exceptions
.http
import Conflict
42 from keystoneclient
.v3
import client
43 from http
import HTTPStatus
44 from osm_nbi
.validation
import is_valid_uuid
47 class AuthconnKeystone(Authconn
):
48 def __init__(self
, config
, db
):
49 Authconn
.__init
__(self
, config
, db
)
51 self
.logger
= logging
.getLogger("nbi.authenticator.keystone")
53 self
.auth_url
= "http://{0}:{1}/v3".format(config
.get("auth_url", "keystone"), config
.get("auth_port", "5000"))
54 self
.user_domain_name_list
= config
.get("user_domain_name", "default")
55 self
.user_domain_name_list
= self
.user_domain_name_list
.split(",")
56 self
.admin_project
= config
.get("service_project", "service")
57 self
.admin_username
= config
.get("service_username", "nbi")
58 self
.admin_password
= config
.get("service_password", "nbi")
59 self
.project_domain_name_list
= config
.get("project_domain_name", "default")
60 self
.project_domain_name_list
= self
.project_domain_name_list
.split(",")
61 if len(self
.user_domain_name_list
) != len(self
.project_domain_name_list
):
62 raise ValueError("Invalid configuration parameter fo authenticate. 'project_domain_name' and "
63 "'user_domain_name' must be a comma-separated list with the same size. Revise "
64 "configuration or/and 'OSMNBI_AUTHENTICATION_PROJECT_DOMAIN_NAME', "
65 "'OSMNBI_AUTHENTICATION_USER_DOMAIN_NAME' Variables")
67 # Waiting for Keystone to be up
70 while available
is None:
73 result
= requests
.get(self
.auth_url
)
74 available
= True if result
.status_code
== 200 else None
78 raise AuthException("Keystone not available after 300s timeout")
80 self
.auth
= v3
.Password(user_domain_name
=self
.user_domain_name_list
[0],
81 username
=self
.admin_username
,
82 password
=self
.admin_password
,
83 project_domain_name
=self
.project_domain_name_list
[0],
84 project_name
=self
.admin_project
,
85 auth_url
=self
.auth_url
)
86 self
.sess
= session
.Session(auth
=self
.auth
)
87 self
.keystone
= client
.Client(session
=self
.sess
)
89 def authenticate(self
, credentials
, token_info
=None):
91 Authenticate a user using username/password or token_info, plus project
92 :param credentials: dictionary that contains:
93 username: name, id or None
94 password: password or None
95 project_id: name, id, or None. If None first found project will be used to get an scope token
96 project_domain_name: (Optional) To use a concrete domain for the project
97 user_domain_name: (Optional) To use a concrete domain for the project
98 other items are allowed and ignored
99 :param token_info: previous token_info to obtain authorization
100 :return: the scoped token info or raises an exception. The token is a dictionary with:
101 _id: token string id,
103 project_id: scoped_token project_id,
104 project_name: scoped_token project_name,
105 expires: epoch time when it expires,
112 if credentials
.get("project_domain_name"):
113 project_domain_name_list
= (credentials
["project_domain_name"], )
115 project_domain_name_list
= self
.project_domain_name_list
116 if credentials
.get("user_domain_name"):
117 user_domain_name_list
= (credentials
["user_domain_name"], )
119 user_domain_name_list
= self
.user_domain_name_list
121 for index
, project_domain_name
in enumerate(project_domain_name_list
):
122 user_domain_name
= user_domain_name_list
[index
]
124 if credentials
.get("username"):
125 if is_valid_uuid(credentials
["username"]):
126 user_id
= credentials
["username"]
128 username
= credentials
["username"]
130 # get an unscoped token firstly
131 unscoped_token
= self
.keystone
.get_raw_token_from_identity_service(
132 auth_url
=self
.auth_url
,
135 password
=credentials
.get("password"),
136 user_domain_name
=user_domain_name
,
137 project_domain_name
=project_domain_name
)
139 unscoped_token
= self
.keystone
.tokens
.validate(token
=token_info
.get("_id"))
141 raise AuthException("Provide credentials: username/password or Authorization Bearer token",
142 http_code
=HTTPStatus
.UNAUTHORIZED
)
144 if not credentials
.get("project_id"):
145 # get first project for the user
146 project_list
= self
.keystone
.projects
.list(user
=unscoped_token
["user"]["id"])
148 raise AuthException("The user {} has not any project and cannot be used for authentication".
149 format(credentials
.get("username")), http_code
=HTTPStatus
.UNAUTHORIZED
)
150 project_id
= project_list
[0].id
152 if is_valid_uuid(credentials
["project_id"]):
153 project_id
= credentials
["project_id"]
155 project_name
= credentials
["project_id"]
157 scoped_token
= self
.keystone
.get_raw_token_from_identity_service(
158 auth_url
=self
.auth_url
,
159 project_name
=project_name
,
160 project_id
=project_id
,
161 user_domain_name
=user_domain_name
,
162 project_domain_name
=project_domain_name
,
163 token
=unscoped_token
["auth_token"])
166 "_id": scoped_token
.auth_token
,
167 "id": scoped_token
.auth_token
,
168 "user_id": scoped_token
.user_id
,
169 "username": scoped_token
.username
,
170 "project_id": scoped_token
.project_id
,
171 "project_name": scoped_token
.project_name
,
172 "project_domain_name": scoped_token
.project_domain_name
,
173 "user_domain_name": scoped_token
.user_domain_name
,
174 "expires": scoped_token
.expires
.timestamp(),
175 "issued_at": scoped_token
.issued
.timestamp()
179 except ClientException
as e
:
180 if index
>= len(user_domain_name_list
)-1 or index
>= len(project_domain_name_list
)-1:
181 # if last try, launch exception
182 # self.logger.exception("Error during user authentication using keystone: {}".format(e))
183 raise AuthException("Error during user authentication using Keystone: {}".format(e
),
184 http_code
=HTTPStatus
.UNAUTHORIZED
)
186 def validate_token(self
, token
):
188 Check if the token is valid.
190 :param token: token id to be validated
191 :return: dictionary with information associated with the token:
194 "project_id": project_id,
196 "roles": list with dict containing {name, id}
197 If the token is not valid an exception is raised.
203 token_info
= self
.keystone
.tokens
.validate(token
=token
)
205 "_id": token_info
["auth_token"],
206 "id": token_info
["auth_token"],
207 "project_id": token_info
["project"]["id"],
208 "project_name": token_info
["project"]["name"],
209 "user_id": token_info
["user"]["id"],
210 "username": token_info
["user"]["name"],
211 "roles": token_info
["roles"],
212 "expires": token_info
.expires
.timestamp(),
213 "issued_at": token_info
.issued
.timestamp()
217 except ClientException
as e
:
218 # self.logger.exception("Error during token validation using keystone: {}".format(e))
219 raise AuthException("Error during token validation using Keystone: {}".format(e
),
220 http_code
=HTTPStatus
.UNAUTHORIZED
)
222 def revoke_token(self
, token
):
226 :param token: token to be revoked
229 self
.logger
.info("Revoking token: " + token
)
230 self
.keystone
.tokens
.revoke_token(token
=token
)
233 except ClientException
as e
:
234 # self.logger.exception("Error during token revocation using keystone: {}".format(e))
235 raise AuthException("Error during token revocation using Keystone: {}".format(e
),
236 http_code
=HTTPStatus
.UNAUTHORIZED
)
238 def create_user(self
, user_info
):
242 :param user_info: full user info.
243 :raises AuthconnOperationException: if user creation failed.
244 :return: returns the id of the user in keystone.
247 new_user
= self
.keystone
.users
.create(
248 user_info
["username"], password
=user_info
["password"],
249 domain
=user_info
.get("user_domain_name", self
.user_domain_name_list
[0]),
250 _admin
=user_info
["_admin"])
251 if "project_role_mappings" in user_info
.keys():
252 for mapping
in user_info
["project_role_mappings"]:
253 self
.assign_role_to_user(new_user
.id, mapping
["project"], mapping
["role"])
254 return {"username": new_user
.name
, "_id": new_user
.id}
255 except Conflict
as e
:
256 # self.logger.exception("Error during user creation using keystone: {}".format(e))
257 raise AuthconnOperationException(e
, http_code
=HTTPStatus
.CONFLICT
)
258 except ClientException
as e
:
259 # self.logger.exception("Error during user creation using keystone: {}".format(e))
260 raise AuthconnOperationException("Error during user creation using Keystone: {}".format(e
))
262 def update_user(self
, user_info
):
264 Change the user name and/or password.
266 :param user_info: user info modifications
267 :raises AuthconnOperationException: if change failed.
270 user
= user_info
.get("_id") or user_info
.get("username")
271 if is_valid_uuid(user
):
272 user_obj_list
= [self
.keystone
.users
.get(user
)]
274 user_obj_list
= self
.keystone
.users
.list(name
=user
)
275 if not user_obj_list
:
276 raise AuthconnNotFoundException("User '{}' not found".format(user
))
277 user_obj
= user_obj_list
[0]
278 user_id
= user_obj
.id
279 if user_info
.get("password") or user_info
.get("username") \
280 or user_info
.get("add_project_role_mappings") or user_info
.get("remove_project_role_mappings"):
281 ctime
= user_obj
._admin
.get("created", 0) if hasattr(user_obj
, "_admin") else 0
282 self
.keystone
.users
.update(user_id
, password
=user_info
.get("password"), name
=user_info
.get("username"),
283 _admin
={"created": ctime
, "modified": time
.time()})
284 for mapping
in user_info
.get("remove_project_role_mappings", []):
285 self
.remove_role_from_user(user_id
, mapping
["project"], mapping
["role"])
286 for mapping
in user_info
.get("add_project_role_mappings", []):
287 self
.assign_role_to_user(user_id
, mapping
["project"], mapping
["role"])
288 except ClientException
as e
:
289 # self.logger.exception("Error during user password/name update using keystone: {}".format(e))
290 raise AuthconnOperationException("Error during user update using Keystone: {}".format(e
))
292 def delete_user(self
, user_id
):
296 :param user_id: user identifier.
297 :raises AuthconnOperationException: if user deletion failed.
300 result
, detail
= self
.keystone
.users
.delete(user_id
)
301 if result
.status_code
!= 204:
302 raise ClientException("error {} {}".format(result
.status_code
, detail
))
304 except ClientException
as e
:
305 # self.logger.exception("Error during user deletion using keystone: {}".format(e))
306 raise AuthconnOperationException("Error during user deletion using Keystone: {}".format(e
))
308 def get_user_list(self
, filter_q
=None):
312 :param filter_q: dictionary to filter user list by name (username is also admited) and/or _id
313 :return: returns a list of users.
318 filter_name
= filter_q
.get("name") or filter_q
.get("username")
319 users
= self
.keystone
.users
.list(name
=filter_name
)
321 "username": user
.name
,
324 "_admin": user
.to_dict().get("_admin", {}) # TODO: REVISE
325 } for user
in users
if user
.name
!= self
.admin_username
]
327 if filter_q
and filter_q
.get("_id"):
328 users
= [user
for user
in users
if filter_q
["_id"] == user
["_id"]]
331 user
["project_role_mappings"] = []
332 user
["projects"] = []
333 projects
= self
.keystone
.projects
.list(user
=user
["_id"])
334 for project
in projects
:
335 user
["projects"].append(project
.name
)
337 roles
= self
.keystone
.roles
.list(user
=user
["_id"], project
=project
.id)
340 "project": project
.id,
341 "project_name": project
.name
,
342 "role_name": role
.name
,
345 user
["project_role_mappings"].append(prm
)
348 except ClientException
as e
:
349 # self.logger.exception("Error during user listing using keystone: {}".format(e))
350 raise AuthconnOperationException("Error during user listing using Keystone: {}".format(e
))
352 def get_role_list(self
, filter_q
=None):
356 :param filter_q: dictionary to filter role list by _id and/or name.
357 :return: returns the list of roles.
362 filter_name
= filter_q
.get("name")
363 roles_list
= self
.keystone
.roles
.list(name
=filter_name
)
368 "_admin": role
.to_dict().get("_admin", {}),
369 "permissions": role
.to_dict().get("permissions", {})
370 } for role
in roles_list
if role
.name
!= "service"]
372 if filter_q
and filter_q
.get("_id"):
373 roles
= [role
for role
in roles
if filter_q
["_id"] == role
["_id"]]
376 except ClientException
as e
:
377 # self.logger.exception("Error during user role listing using keystone: {}".format(e))
378 raise AuthException("Error during user role listing using Keystone: {}".format(e
),
379 http_code
=HTTPStatus
.UNAUTHORIZED
)
381 def create_role(self
, role_info
):
385 :param role_info: full role info.
386 :raises AuthconnOperationException: if role creation failed.
389 result
= self
.keystone
.roles
.create(role_info
["name"], permissions
=role_info
.get("permissions"),
390 _admin
=role_info
.get("_admin"))
392 except Conflict
as ex
:
393 raise AuthconnConflictException(str(ex
))
394 except ClientException
as e
:
395 # self.logger.exception("Error during role creation using keystone: {}".format(e))
396 raise AuthconnOperationException("Error during role creation using Keystone: {}".format(e
))
398 def delete_role(self
, role_id
):
402 :param role_id: role identifier.
403 :raises AuthconnOperationException: if role deletion failed.
406 result
, detail
= self
.keystone
.roles
.delete(role_id
)
408 if result
.status_code
!= 204:
409 raise ClientException("error {} {}".format(result
.status_code
, detail
))
412 except ClientException
as e
:
413 # self.logger.exception("Error during role deletion using keystone: {}".format(e))
414 raise AuthconnOperationException("Error during role deletion using Keystone: {}".format(e
))
416 def update_role(self
, role_info
):
418 Change the name of a role
419 :param role_info: full role info
423 rid
= role_info
["_id"]
424 if not is_valid_uuid(rid
): # Is this required?
425 role_obj_list
= self
.keystone
.roles
.list(name
=rid
)
426 if not role_obj_list
:
427 raise AuthconnNotFoundException("Role '{}' not found".format(rid
))
428 rid
= role_obj_list
[0].id
429 self
.keystone
.roles
.update(rid
, name
=role_info
["name"], permissions
=role_info
.get("permissions"),
430 _admin
=role_info
.get("_admin"))
431 except ClientException
as e
:
432 # self.logger.exception("Error during role update using keystone: {}".format(e))
433 raise AuthconnOperationException("Error during role updating using Keystone: {}".format(e
))
435 def get_project_list(self
, filter_q
=None):
437 Get all the projects.
439 :param filter_q: dictionary to filter project list.
440 :return: list of projects
445 filter_name
= filter_q
.get("name")
446 projects
= self
.keystone
.projects
.list(name
=filter_name
)
449 "name": project
.name
,
451 "_admin": project
.to_dict().get("_admin", {}), # TODO: REVISE
452 "quotas": project
.to_dict().get("quotas", {}), # TODO: REVISE
453 } for project
in projects
]
455 if filter_q
and filter_q
.get("_id"):
456 projects
= [project
for project
in projects
457 if filter_q
["_id"] == project
["_id"]]
460 except ClientException
as e
:
461 # self.logger.exception("Error during user project listing using keystone: {}".format(e))
462 raise AuthException("Error during user project listing using Keystone: {}".format(e
),
463 http_code
=HTTPStatus
.UNAUTHORIZED
)
465 def create_project(self
, project_info
):
469 :param project_info: full project info.
470 :return: the internal id of the created project
471 :raises AuthconnOperationException: if project creation failed.
474 result
= self
.keystone
.projects
.create(
475 project_info
["name"],
476 project_info
.get("project_domain_name", self
.project_domain_name_list
[0]),
477 _admin
=project_info
["_admin"],
478 quotas
=project_info
.get("quotas", {})
481 except ClientException
as e
:
482 # self.logger.exception("Error during project creation using keystone: {}".format(e))
483 raise AuthconnOperationException("Error during project creation using Keystone: {}".format(e
))
485 def delete_project(self
, project_id
):
489 :param project_id: project identifier.
490 :raises AuthconnOperationException: if project deletion failed.
493 # projects = self.keystone.projects.list()
494 # project_obj = [project for project in projects if project.id == project_id][0]
495 # result, _ = self.keystone.projects.delete(project_obj)
497 result
, detail
= self
.keystone
.projects
.delete(project_id
)
498 if result
.status_code
!= 204:
499 raise ClientException("error {} {}".format(result
.status_code
, detail
))
502 except ClientException
as e
:
503 # self.logger.exception("Error during project deletion using keystone: {}".format(e))
504 raise AuthconnOperationException("Error during project deletion using Keystone: {}".format(e
))
506 def update_project(self
, project_id
, project_info
):
508 Change the name of a project
509 :param project_id: project to be changed
510 :param project_info: full project info
514 self
.keystone
.projects
.update(project_id
, name
=project_info
["name"],
515 _admin
=project_info
["_admin"],
516 quotas
=project_info
.get("quotas", {})
518 except ClientException
as e
:
519 # self.logger.exception("Error during project update using keystone: {}".format(e))
520 raise AuthconnOperationException("Error during project update using Keystone: {}".format(e
))
522 def assign_role_to_user(self
, user
, project
, role
):
524 Assigning a role to a user in a project.
526 :param user: username.
527 :param project: project name.
528 :param role: role name.
529 :raises AuthconnOperationException: if role assignment failed.
532 if is_valid_uuid(user
):
533 user_obj
= self
.keystone
.users
.get(user
)
535 user_obj_list
= self
.keystone
.users
.list(name
=user
)
536 if not user_obj_list
:
537 raise AuthconnNotFoundException("User '{}' not found".format(user
))
538 user_obj
= user_obj_list
[0]
540 if is_valid_uuid(project
):
541 project_obj
= self
.keystone
.projects
.get(project
)
543 project_obj_list
= self
.keystone
.projects
.list(name
=project
)
544 if not project_obj_list
:
545 raise AuthconnNotFoundException("Project '{}' not found".format(project
))
546 project_obj
= project_obj_list
[0]
548 if is_valid_uuid(role
):
549 role_obj
= self
.keystone
.roles
.get(role
)
551 role_obj_list
= self
.keystone
.roles
.list(name
=role
)
552 if not role_obj_list
:
553 raise AuthconnNotFoundException("Role '{}' not found".format(role
))
554 role_obj
= role_obj_list
[0]
556 self
.keystone
.roles
.grant(role_obj
, user
=user_obj
, project
=project_obj
)
557 except ClientException
as e
:
558 # self.logger.exception("Error during user role assignment using keystone: {}".format(e))
559 raise AuthconnOperationException("Error during role '{}' assignment to user '{}' and project '{}' using "
560 "Keystone: {}".format(role
, user
, project
, e
))
562 def remove_role_from_user(self
, user
, project
, role
):
564 Remove a role from a user in a project.
566 :param user: username.
567 :param project: project name or id.
568 :param role: role name or id.
570 :raises AuthconnOperationException: if role assignment revocation failed.
573 if is_valid_uuid(user
):
574 user_obj
= self
.keystone
.users
.get(user
)
576 user_obj_list
= self
.keystone
.users
.list(name
=user
)
577 if not user_obj_list
:
578 raise AuthconnNotFoundException("User '{}' not found".format(user
))
579 user_obj
= user_obj_list
[0]
581 if is_valid_uuid(project
):
582 project_obj
= self
.keystone
.projects
.get(project
)
584 project_obj_list
= self
.keystone
.projects
.list(name
=project
)
585 if not project_obj_list
:
586 raise AuthconnNotFoundException("Project '{}' not found".format(project
))
587 project_obj
= project_obj_list
[0]
589 if is_valid_uuid(role
):
590 role_obj
= self
.keystone
.roles
.get(role
)
592 role_obj_list
= self
.keystone
.roles
.list(name
=role
)
593 if not role_obj_list
:
594 raise AuthconnNotFoundException("Role '{}' not found".format(role
))
595 role_obj
= role_obj_list
[0]
597 self
.keystone
.roles
.revoke(role_obj
, user
=user_obj
, project
=project_obj
)
598 except ClientException
as e
:
599 # self.logger.exception("Error during user role revocation using keystone: {}".format(e))
600 raise AuthconnOperationException("Error during role '{}' revocation to user '{}' and project '{}' using "
601 "Keystone: {}".format(role
, user
, project
, e
))