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
, token_cache
):
49 Authconn
.__init
__(self
, config
, db
, token_cache
)
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
= config
.get("user_domain_name", "default")
55 self
.admin_project
= config
.get("service_project", "service")
56 self
.admin_username
= config
.get("service_username", "nbi")
57 self
.admin_password
= config
.get("service_password", "nbi")
58 self
.project_domain_name
= config
.get("project_domain_name", "default")
60 # Waiting for Keystone to be up
63 while available
is None:
66 result
= requests
.get(self
.auth_url
)
67 available
= True if result
.status_code
== 200 else None
71 raise AuthException("Keystone not available after 300s timeout")
73 self
.auth
= v3
.Password(user_domain_name
=self
.user_domain_name
,
74 username
=self
.admin_username
,
75 password
=self
.admin_password
,
76 project_domain_name
=self
.project_domain_name
,
77 project_name
=self
.admin_project
,
78 auth_url
=self
.auth_url
)
79 self
.sess
= session
.Session(auth
=self
.auth
)
80 self
.keystone
= client
.Client(session
=self
.sess
)
82 def authenticate(self
, user
, password
, project
=None, token_info
=None):
84 Authenticate a user using username/password or token_info, plus project
85 :param user: user: name, id or None
86 :param password: password or None
87 :param project: name, id, or None. If None first found project will be used to get an scope token
88 :param token_info: previous token_info to obtain authorization
89 :return: the scoped token info or raises an exception. The token is a dictionary with:
92 project_id: scoped_token project_id,
93 project_name: scoped_token project_name,
94 expires: epoch time when it expires,
104 if is_valid_uuid(user
):
109 # get an unscoped token firstly
110 unscoped_token
= self
.keystone
.get_raw_token_from_identity_service(
111 auth_url
=self
.auth_url
,
115 user_domain_name
=self
.user_domain_name
,
116 project_domain_name
=self
.project_domain_name
)
118 unscoped_token
= self
.keystone
.tokens
.validate(token
=token_info
.get("_id"))
120 raise AuthException("Provide credentials: username/password or Authorization Bearer token",
121 http_code
=HTTPStatus
.UNAUTHORIZED
)
124 # get first project for the user
125 project_list
= self
.keystone
.projects
.list(user
=unscoped_token
["user"]["id"])
127 raise AuthException("The user {} has not any project and cannot be used for authentication".
128 format(user
), http_code
=HTTPStatus
.UNAUTHORIZED
)
129 project_id
= project_list
[0].id
131 if is_valid_uuid(project
):
134 project_name
= project
136 scoped_token
= self
.keystone
.get_raw_token_from_identity_service(
137 auth_url
=self
.auth_url
,
138 project_name
=project_name
,
139 project_id
=project_id
,
140 user_domain_name
=self
.user_domain_name
,
141 project_domain_name
=self
.project_domain_name
,
142 token
=unscoped_token
["auth_token"])
145 "_id": scoped_token
.auth_token
,
146 "id": scoped_token
.auth_token
,
147 "user_id": scoped_token
.user_id
,
148 "username": scoped_token
.username
,
149 "project_id": scoped_token
.project_id
,
150 "project_name": scoped_token
.project_name
,
151 "expires": scoped_token
.expires
.timestamp(),
152 "issued_at": scoped_token
.issued
.timestamp()
156 except ClientException
as e
:
157 # self.logger.exception("Error during user authentication using keystone. Method: basic: {}".format(e))
158 raise AuthException("Error during user authentication using Keystone: {}".format(e
),
159 http_code
=HTTPStatus
.UNAUTHORIZED
)
161 def validate_token(self
, token
):
163 Check if the token is valid.
165 :param token: token id to be validated
166 :return: dictionary with information associated with the token:
169 "project_id": project_id,
171 "roles": list with dict containing {name, id}
172 If the token is not valid an exception is raised.
178 token_info
= self
.keystone
.tokens
.validate(token
=token
)
180 "_id": token_info
["auth_token"],
181 "id": token_info
["auth_token"],
182 "project_id": token_info
["project"]["id"],
183 "project_name": token_info
["project"]["name"],
184 "user_id": token_info
["user"]["id"],
185 "username": token_info
["user"]["name"],
186 "roles": token_info
["roles"],
187 "expires": token_info
.expires
.timestamp(),
188 "issued_at": token_info
.issued
.timestamp()
192 except ClientException
as e
:
193 # self.logger.exception("Error during token validation using keystone: {}".format(e))
194 raise AuthException("Error during token validation using Keystone: {}".format(e
),
195 http_code
=HTTPStatus
.UNAUTHORIZED
)
197 def revoke_token(self
, token
):
201 :param token: token to be revoked
204 self
.logger
.info("Revoking token: " + token
)
205 self
.keystone
.tokens
.revoke_token(token
=token
)
208 except ClientException
as e
:
209 # self.logger.exception("Error during token revocation using keystone: {}".format(e))
210 raise AuthException("Error during token revocation using Keystone: {}".format(e
),
211 http_code
=HTTPStatus
.UNAUTHORIZED
)
213 def create_user(self
, user_info
):
217 :param user_info: full user info.
218 :raises AuthconnOperationException: if user creation failed.
219 :return: returns the id of the user in keystone.
222 new_user
= self
.keystone
.users
.create(user_info
["username"], password
=user_info
["password"],
223 domain
=self
.user_domain_name
, _admin
=user_info
["_admin"])
224 if "project_role_mappings" in user_info
.keys():
225 for mapping
in user_info
["project_role_mappings"]:
226 self
.assign_role_to_user(new_user
.id, mapping
["project"], mapping
["role"])
227 return {"username": new_user
.name
, "_id": new_user
.id}
228 except Conflict
as e
:
229 # self.logger.exception("Error during user creation using keystone: {}".format(e))
230 raise AuthconnOperationException(e
, http_code
=HTTPStatus
.CONFLICT
)
231 except ClientException
as e
:
232 # self.logger.exception("Error during user creation using keystone: {}".format(e))
233 raise AuthconnOperationException("Error during user creation using Keystone: {}".format(e
))
235 def update_user(self
, user_info
):
237 Change the user name and/or password.
239 :param user_info: user info modifications
240 :raises AuthconnOperationException: if change failed.
243 user
= user_info
.get("_id") or user_info
.get("username")
244 if is_valid_uuid(user
):
245 user_obj_list
= [self
.keystone
.users
.get(user
)]
247 user_obj_list
= self
.keystone
.users
.list(name
=user
)
248 if not user_obj_list
:
249 raise AuthconnNotFoundException("User '{}' not found".format(user
))
250 user_obj
= user_obj_list
[0]
251 user_id
= user_obj
.id
252 if user_info
.get("password") or user_info
.get("username") \
253 or user_info
.get("add_project_role_mappings") or user_info
.get("remove_project_role_mappings"):
254 self
.keystone
.users
.update(user_id
, password
=user_info
.get("password"), name
=user_info
.get("username"),
255 _admin
={"created": user_obj
._admin
["created"], "modified": time
.time()})
256 for mapping
in user_info
.get("remove_project_role_mappings", []):
257 self
.remove_role_from_user(user_id
, mapping
["project"], mapping
["role"])
258 for mapping
in user_info
.get("add_project_role_mappings", []):
259 self
.assign_role_to_user(user_id
, mapping
["project"], mapping
["role"])
260 except ClientException
as e
:
261 # self.logger.exception("Error during user password/name update using keystone: {}".format(e))
262 raise AuthconnOperationException("Error during user update using Keystone: {}".format(e
))
264 def delete_user(self
, user_id
):
268 :param user_id: user identifier.
269 :raises AuthconnOperationException: if user deletion failed.
272 result
, detail
= self
.keystone
.users
.delete(user_id
)
273 if result
.status_code
!= 204:
274 raise ClientException("error {} {}".format(result
.status_code
, detail
))
276 except ClientException
as e
:
277 # self.logger.exception("Error during user deletion using keystone: {}".format(e))
278 raise AuthconnOperationException("Error during user deletion using Keystone: {}".format(e
))
280 def get_user_list(self
, filter_q
=None):
284 :param filter_q: dictionary to filter user list by name (username is also admited) and/or _id
285 :return: returns a list of users.
290 filter_name
= filter_q
.get("name") or filter_q
.get("username")
291 users
= self
.keystone
.users
.list(name
=filter_name
)
293 "username": user
.name
,
296 "_admin": user
.to_dict().get("_admin", {}) # TODO: REVISE
297 } for user
in users
if user
.name
!= self
.admin_username
]
299 if filter_q
and filter_q
.get("_id"):
300 users
= [user
for user
in users
if filter_q
["_id"] == user
["_id"]]
303 user
["project_role_mappings"] = []
304 user
["projects"] = []
305 projects
= self
.keystone
.projects
.list(user
=user
["_id"])
306 for project
in projects
:
307 user
["projects"].append(project
.name
)
309 roles
= self
.keystone
.roles
.list(user
=user
["_id"], project
=project
.id)
312 "project": project
.id,
313 "project_name": project
.name
,
314 "role_name": role
.name
,
317 user
["project_role_mappings"].append(prm
)
320 except ClientException
as e
:
321 # self.logger.exception("Error during user listing using keystone: {}".format(e))
322 raise AuthconnOperationException("Error during user listing using Keystone: {}".format(e
))
324 def get_role_list(self
, filter_q
=None):
328 :param filter_q: dictionary to filter role list by _id and/or name.
329 :return: returns the list of roles.
334 filter_name
= filter_q
.get("name")
335 roles_list
= self
.keystone
.roles
.list(name
=filter_name
)
340 "_admin": role
.to_dict().get("_admin", {}),
341 "permissions": role
.to_dict().get("permissions", {})
342 } for role
in roles_list
if role
.name
!= "service"]
344 if filter_q
and filter_q
.get("_id"):
345 roles
= [role
for role
in roles
if filter_q
["_id"] == role
["_id"]]
348 except ClientException
as e
:
349 # self.logger.exception("Error during user role listing using keystone: {}".format(e))
350 raise AuthException("Error during user role listing using Keystone: {}".format(e
),
351 http_code
=HTTPStatus
.UNAUTHORIZED
)
353 def create_role(self
, role_info
):
357 :param role_info: full role info.
358 :raises AuthconnOperationException: if role creation failed.
361 result
= self
.keystone
.roles
.create(role_info
["name"], permissions
=role_info
.get("permissions"),
362 _admin
=role_info
.get("_admin"))
364 except Conflict
as ex
:
365 raise AuthconnConflictException(str(ex
))
366 except ClientException
as e
:
367 # self.logger.exception("Error during role creation using keystone: {}".format(e))
368 raise AuthconnOperationException("Error during role creation using Keystone: {}".format(e
))
370 def delete_role(self
, role_id
):
374 :param role_id: role identifier.
375 :raises AuthconnOperationException: if role deletion failed.
378 result
, detail
= self
.keystone
.roles
.delete(role_id
)
380 if result
.status_code
!= 204:
381 raise ClientException("error {} {}".format(result
.status_code
, detail
))
384 except ClientException
as e
:
385 # self.logger.exception("Error during role deletion using keystone: {}".format(e))
386 raise AuthconnOperationException("Error during role deletion using Keystone: {}".format(e
))
388 def update_role(self
, role_info
):
390 Change the name of a role
391 :param role_info: full role info
395 rid
= role_info
["_id"]
396 if not is_valid_uuid(rid
): # Is this required?
397 role_obj_list
= self
.keystone
.roles
.list(name
=rid
)
398 if not role_obj_list
:
399 raise AuthconnNotFoundException("Role '{}' not found".format(rid
))
400 rid
= role_obj_list
[0].id
401 self
.keystone
.roles
.update(rid
, name
=role_info
["name"], permissions
=role_info
.get("permissions"),
402 _admin
=role_info
.get("_admin"))
403 except ClientException
as e
:
404 # self.logger.exception("Error during role update using keystone: {}".format(e))
405 raise AuthconnOperationException("Error during role updating using Keystone: {}".format(e
))
407 def get_project_list(self
, filter_q
=None):
409 Get all the projects.
411 :param filter_q: dictionary to filter project list.
412 :return: list of projects
417 filter_name
= filter_q
.get("name")
418 projects
= self
.keystone
.projects
.list(name
=filter_name
)
421 "name": project
.name
,
423 "_admin": project
.to_dict().get("_admin", {}), # TODO: REVISE
424 "quotas": project
.to_dict().get("quotas", {}), # TODO: REVISE
425 } for project
in projects
]
427 if filter_q
and filter_q
.get("_id"):
428 projects
= [project
for project
in projects
429 if filter_q
["_id"] == project
["_id"]]
432 except ClientException
as e
:
433 # self.logger.exception("Error during user project listing using keystone: {}".format(e))
434 raise AuthException("Error during user project listing using Keystone: {}".format(e
),
435 http_code
=HTTPStatus
.UNAUTHORIZED
)
437 def create_project(self
, project_info
):
441 :param project_info: full project info.
442 :return: the internal id of the created project
443 :raises AuthconnOperationException: if project creation failed.
446 result
= self
.keystone
.projects
.create(project_info
["name"], self
.project_domain_name
,
447 _admin
=project_info
["_admin"],
448 quotas
=project_info
.get("quotas", {})
451 except ClientException
as e
:
452 # self.logger.exception("Error during project creation using keystone: {}".format(e))
453 raise AuthconnOperationException("Error during project creation using Keystone: {}".format(e
))
455 def delete_project(self
, project_id
):
459 :param project_id: project identifier.
460 :raises AuthconnOperationException: if project deletion failed.
463 # projects = self.keystone.projects.list()
464 # project_obj = [project for project in projects if project.id == project_id][0]
465 # result, _ = self.keystone.projects.delete(project_obj)
467 result
, detail
= self
.keystone
.projects
.delete(project_id
)
468 if result
.status_code
!= 204:
469 raise ClientException("error {} {}".format(result
.status_code
, detail
))
472 except ClientException
as e
:
473 # self.logger.exception("Error during project deletion using keystone: {}".format(e))
474 raise AuthconnOperationException("Error during project deletion using Keystone: {}".format(e
))
476 def update_project(self
, project_id
, project_info
):
478 Change the name of a project
479 :param project_id: project to be changed
480 :param project_info: full project info
484 self
.keystone
.projects
.update(project_id
, name
=project_info
["name"],
485 _admin
=project_info
["_admin"],
486 quotas
=project_info
.get("quotas", {})
488 except ClientException
as e
:
489 # self.logger.exception("Error during project update using keystone: {}".format(e))
490 raise AuthconnOperationException("Error during project update using Keystone: {}".format(e
))
492 def assign_role_to_user(self
, user
, project
, role
):
494 Assigning a role to a user in a project.
496 :param user: username.
497 :param project: project name.
498 :param role: role name.
499 :raises AuthconnOperationException: if role assignment failed.
502 if is_valid_uuid(user
):
503 user_obj
= self
.keystone
.users
.get(user
)
505 user_obj_list
= self
.keystone
.users
.list(name
=user
)
506 if not user_obj_list
:
507 raise AuthconnNotFoundException("User '{}' not found".format(user
))
508 user_obj
= user_obj_list
[0]
510 if is_valid_uuid(project
):
511 project_obj
= self
.keystone
.projects
.get(project
)
513 project_obj_list
= self
.keystone
.projects
.list(name
=project
)
514 if not project_obj_list
:
515 raise AuthconnNotFoundException("Project '{}' not found".format(project
))
516 project_obj
= project_obj_list
[0]
518 if is_valid_uuid(role
):
519 role_obj
= self
.keystone
.roles
.get(role
)
521 role_obj_list
= self
.keystone
.roles
.list(name
=role
)
522 if not role_obj_list
:
523 raise AuthconnNotFoundException("Role '{}' not found".format(role
))
524 role_obj
= role_obj_list
[0]
526 self
.keystone
.roles
.grant(role_obj
, user
=user_obj
, project
=project_obj
)
527 except ClientException
as e
:
528 # self.logger.exception("Error during user role assignment using keystone: {}".format(e))
529 raise AuthconnOperationException("Error during role '{}' assignment to user '{}' and project '{}' using "
530 "Keystone: {}".format(role
, user
, project
, e
))
532 def remove_role_from_user(self
, user
, project
, role
):
534 Remove a role from a user in a project.
536 :param user: username.
537 :param project: project name or id.
538 :param role: role name or id.
540 :raises AuthconnOperationException: if role assignment revocation failed.
543 if is_valid_uuid(user
):
544 user_obj
= self
.keystone
.users
.get(user
)
546 user_obj_list
= self
.keystone
.users
.list(name
=user
)
547 if not user_obj_list
:
548 raise AuthconnNotFoundException("User '{}' not found".format(user
))
549 user_obj
= user_obj_list
[0]
551 if is_valid_uuid(project
):
552 project_obj
= self
.keystone
.projects
.get(project
)
554 project_obj_list
= self
.keystone
.projects
.list(name
=project
)
555 if not project_obj_list
:
556 raise AuthconnNotFoundException("Project '{}' not found".format(project
))
557 project_obj
= project_obj_list
[0]
559 if is_valid_uuid(role
):
560 role_obj
= self
.keystone
.roles
.get(role
)
562 role_obj_list
= self
.keystone
.roles
.list(name
=role
)
563 if not role_obj_list
:
564 raise AuthconnNotFoundException("Role '{}' not found".format(role
))
565 role_obj
= role_obj_list
[0]
567 self
.keystone
.roles
.revoke(role_obj
, user
=user_obj
, project
=project_obj
)
568 except ClientException
as e
:
569 # self.logger.exception("Error during user role revocation using keystone: {}".format(e))
570 raise AuthconnOperationException("Error during role '{}' revocation to user '{}' and project '{}' using "
571 "Keystone: {}".format(role
, user
, project
, e
))