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 __date__
= "$27-jul-2018 23:59:59$"
31 from authconn
import Authconn
, AuthException
, AuthconnOperationException
, AuthconnNotFoundException
, \
32 AuthconnConflictException
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
46 class AuthconnKeystone(Authconn
):
47 def __init__(self
, config
, db
, token_cache
):
48 Authconn
.__init
__(self
, config
, db
, token_cache
)
50 self
.logger
= logging
.getLogger("nbi.authenticator.keystone")
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")
59 # Waiting for Keystone to be up
62 while available
is None:
65 result
= requests
.get(self
.auth_url
)
66 available
= True if result
.status_code
== 200 else None
70 raise AuthException("Keystone not available after 300s timeout")
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
)
81 def authenticate(self
, user
, password
, project
=None, token_info
=None):
83 Authenticate a user using username/password or token_info, 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_info: previous token_info to obtain authorization
88 :return: the scoped token info or raises an exception. The token is a dictionary with:
91 project_id: scoped_token project_id,
92 project_name: scoped_token project_name,
93 expires: epoch time when it expires,
103 if is_valid_uuid(user
):
108 # get an unscoped token firstly
109 unscoped_token
= self
.keystone
.get_raw_token_from_identity_service(
110 auth_url
=self
.auth_url
,
114 user_domain_name
=self
.user_domain_name
,
115 project_domain_name
=self
.project_domain_name
)
117 unscoped_token
= self
.keystone
.tokens
.validate(token
=token_info
.get("_id"))
119 raise AuthException("Provide credentials: username/password or Authorization Bearer token",
120 http_code
=HTTPStatus
.UNAUTHORIZED
)
123 # get first project for the user
124 project_list
= self
.keystone
.projects
.list(user
=unscoped_token
["user"]["id"])
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
130 if is_valid_uuid(project
):
133 project_name
= project
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"])
144 "_id": scoped_token
.auth_token
,
145 "id": scoped_token
.auth_token
,
146 "user_id": scoped_token
.user_id
,
147 "username": scoped_token
.username
,
148 "project_id": scoped_token
.project_id
,
149 "project_name": scoped_token
.project_name
,
150 "expires": scoped_token
.expires
.timestamp(),
151 "issued_at": scoped_token
.issued
.timestamp()
155 except ClientException
as e
:
156 # self.logger.exception("Error during user authentication using keystone. Method: basic: {}".format(e))
157 raise AuthException("Error during user authentication using Keystone: {}".format(e
),
158 http_code
=HTTPStatus
.UNAUTHORIZED
)
160 def validate_token(self
, token
):
162 Check if the token is valid.
164 :param token: token id to be validated
165 :return: dictionary with information associated with the token:
168 "project_id": project_id,
170 "roles": list with dict containing {name, id}
171 If the token is not valid an exception is raised.
177 token_info
= self
.keystone
.tokens
.validate(token
=token
)
179 "_id": token_info
["auth_token"],
180 "id": token_info
["auth_token"],
181 "project_id": token_info
["project"]["id"],
182 "project_name": token_info
["project"]["name"],
183 "user_id": token_info
["user"]["id"],
184 "username": token_info
["user"]["name"],
185 "roles": token_info
["roles"],
186 "expires": token_info
.expires
.timestamp(),
187 "issued_at": token_info
.issued
.timestamp()
191 except ClientException
as e
:
192 # self.logger.exception("Error during token validation using keystone: {}".format(e))
193 raise AuthException("Error during token validation using Keystone: {}".format(e
),
194 http_code
=HTTPStatus
.UNAUTHORIZED
)
196 def revoke_token(self
, token
):
200 :param token: token to be revoked
203 self
.logger
.info("Revoking token: " + token
)
204 self
.keystone
.tokens
.revoke_token(token
=token
)
207 except ClientException
as e
:
208 # self.logger.exception("Error during token revocation using keystone: {}".format(e))
209 raise AuthException("Error during token revocation using Keystone: {}".format(e
),
210 http_code
=HTTPStatus
.UNAUTHORIZED
)
212 def create_user(self
, user_info
):
216 :param user_info: full user info.
217 :raises AuthconnOperationException: if user creation failed.
218 :return: returns the id of the user in keystone.
221 new_user
= self
.keystone
.users
.create(user_info
["username"], password
=user_info
["password"],
222 domain
=self
.user_domain_name
, _admin
=user_info
["_admin"])
223 if "project_role_mappings" in user_info
.keys():
224 for mapping
in user_info
["project_role_mappings"]:
225 self
.assign_role_to_user(new_user
.id, mapping
["project"], mapping
["role"])
226 return {"username": new_user
.name
, "_id": new_user
.id}
227 except Conflict
as e
:
228 # self.logger.exception("Error during user creation using keystone: {}".format(e))
229 raise AuthconnOperationException(e
, http_code
=HTTPStatus
.CONFLICT
)
230 except ClientException
as e
:
231 # self.logger.exception("Error during user creation using keystone: {}".format(e))
232 raise AuthconnOperationException("Error during user creation using Keystone: {}".format(e
))
234 def update_user(self
, user_info
):
236 Change the user name and/or password.
238 :param user_info: user info modifications
239 :raises AuthconnOperationException: if change failed.
242 user
= user_info
.get("_id") or user_info
.get("username")
243 if is_valid_uuid(user
):
244 user_obj_list
= [self
.keystone
.users
.get(user
)]
246 user_obj_list
= self
.keystone
.users
.list(name
=user
)
247 if not user_obj_list
:
248 raise AuthconnNotFoundException("User '{}' not found".format(user
))
249 user_obj
= user_obj_list
[0]
250 user_id
= user_obj
.id
251 if user_info
.get("password") or user_info
.get("username") \
252 or user_info
.get("add_project_role_mappings") or user_info
.get("remove_project_role_mappings"):
253 self
.keystone
.users
.update(user_id
, password
=user_info
.get("password"), name
=user_info
.get("username"),
254 _admin
={"created": user_obj
._admin
["created"], "modified": time
.time()})
255 for mapping
in user_info
.get("remove_project_role_mappings", []):
256 self
.remove_role_from_user(user_id
, mapping
["project"], mapping
["role"])
257 for mapping
in user_info
.get("add_project_role_mappings", []):
258 self
.assign_role_to_user(user_id
, mapping
["project"], mapping
["role"])
259 except ClientException
as e
:
260 # self.logger.exception("Error during user password/name update using keystone: {}".format(e))
261 raise AuthconnOperationException("Error during user update using Keystone: {}".format(e
))
263 def delete_user(self
, user_id
):
267 :param user_id: user identifier.
268 :raises AuthconnOperationException: if user deletion failed.
271 result
, detail
= self
.keystone
.users
.delete(user_id
)
272 if result
.status_code
!= 204:
273 raise ClientException("error {} {}".format(result
.status_code
, detail
))
275 except ClientException
as e
:
276 # self.logger.exception("Error during user deletion using keystone: {}".format(e))
277 raise AuthconnOperationException("Error during user deletion using Keystone: {}".format(e
))
279 def get_user_list(self
, filter_q
=None):
283 :param filter_q: dictionary to filter user list by name (username is also admited) and/or _id
284 :return: returns a list of users.
289 filter_name
= filter_q
.get("name") or filter_q
.get("username")
290 users
= self
.keystone
.users
.list(name
=filter_name
)
292 "username": user
.name
,
295 "_admin": user
.to_dict().get("_admin", {}) # TODO: REVISE
296 } for user
in users
if user
.name
!= self
.admin_username
]
298 if filter_q
and filter_q
.get("_id"):
299 users
= [user
for user
in users
if filter_q
["_id"] == user
["_id"]]
302 projects
= self
.keystone
.projects
.list(user
=user
["_id"])
304 "name": project
.name
,
307 } for project
in projects
]
309 for project
in projects
:
310 roles
= self
.keystone
.roles
.list(user
=user
["_id"], project
=project
["_id"])
316 project
["roles"] = roles
318 user
["projects"] = projects
321 except ClientException
as e
:
322 # self.logger.exception("Error during user listing using keystone: {}".format(e))
323 raise AuthconnOperationException("Error during user listing using Keystone: {}".format(e
))
325 def get_role_list(self
, filter_q
=None):
329 :param filter_q: dictionary to filter role list by _id and/or name.
330 :return: returns the list of roles.
335 filter_name
= filter_q
.get("name")
336 roles_list
= self
.keystone
.roles
.list(name
=filter_name
)
341 "_admin": role
.to_dict().get("_admin", {}),
342 "permissions": role
.to_dict().get("permissions", {})
343 } for role
in roles_list
if role
.name
!= "service"]
345 if filter_q
and filter_q
.get("_id"):
346 roles
= [role
for role
in roles
if filter_q
["_id"] == role
["_id"]]
349 except ClientException
as e
:
350 # self.logger.exception("Error during user role listing using keystone: {}".format(e))
351 raise AuthException("Error during user role listing using Keystone: {}".format(e
),
352 http_code
=HTTPStatus
.UNAUTHORIZED
)
354 def create_role(self
, role_info
):
358 :param role_info: full role info.
359 :raises AuthconnOperationException: if role creation failed.
362 result
= self
.keystone
.roles
.create(role_info
["name"], permissions
=role_info
.get("permissions"),
363 _admin
=role_info
.get("_admin"))
365 except Conflict
as ex
:
366 raise AuthconnConflictException(str(ex
))
367 except ClientException
as e
:
368 # self.logger.exception("Error during role creation using keystone: {}".format(e))
369 raise AuthconnOperationException("Error during role creation using Keystone: {}".format(e
))
371 def delete_role(self
, role_id
):
375 :param role_id: role identifier.
376 :raises AuthconnOperationException: if role deletion failed.
379 result
, detail
= self
.keystone
.roles
.delete(role_id
)
381 if result
.status_code
!= 204:
382 raise ClientException("error {} {}".format(result
.status_code
, detail
))
385 except ClientException
as e
:
386 # self.logger.exception("Error during role deletion using keystone: {}".format(e))
387 raise AuthconnOperationException("Error during role deletion using Keystone: {}".format(e
))
389 def update_role(self
, role_info
):
391 Change the name of a role
392 :param role_info: full role info
396 rid
= role_info
["_id"]
397 if not is_valid_uuid(rid
): # Is this required?
398 role_obj_list
= self
.keystone
.roles
.list(name
=rid
)
399 if not role_obj_list
:
400 raise AuthconnNotFoundException("Role '{}' not found".format(rid
))
401 rid
= role_obj_list
[0].id
402 self
.keystone
.roles
.update(rid
, name
=role_info
["name"], permissions
=role_info
.get("permissions"),
403 _admin
=role_info
.get("_admin"))
404 except ClientException
as e
:
405 # self.logger.exception("Error during role update using keystone: {}".format(e))
406 raise AuthconnOperationException("Error during role updating using Keystone: {}".format(e
))
408 def get_project_list(self
, filter_q
=None):
410 Get all the projects.
412 :param filter_q: dictionary to filter project list.
413 :return: list of projects
418 filter_name
= filter_q
.get("name")
419 projects
= self
.keystone
.projects
.list(name
=filter_name
)
422 "name": project
.name
,
424 "_admin": project
.to_dict().get("_admin", {}) # 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"])
449 except ClientException
as e
:
450 # self.logger.exception("Error during project creation using keystone: {}".format(e))
451 raise AuthconnOperationException("Error during project creation using Keystone: {}".format(e
))
453 def delete_project(self
, project_id
):
457 :param project_id: project identifier.
458 :raises AuthconnOperationException: if project deletion failed.
461 # projects = self.keystone.projects.list()
462 # project_obj = [project for project in projects if project.id == project_id][0]
463 # result, _ = self.keystone.projects.delete(project_obj)
465 result
, detail
= self
.keystone
.projects
.delete(project_id
)
466 if result
.status_code
!= 204:
467 raise ClientException("error {} {}".format(result
.status_code
, detail
))
470 except ClientException
as e
:
471 # self.logger.exception("Error during project deletion using keystone: {}".format(e))
472 raise AuthconnOperationException("Error during project deletion using Keystone: {}".format(e
))
474 def update_project(self
, project_id
, project_info
):
476 Change the name of a project
477 :param project_id: project to be changed
478 :param project_info: full project info
482 self
.keystone
.projects
.update(project_id
, name
=project_info
["name"], _admin
=project_info
["_admin"])
483 except ClientException
as e
:
484 # self.logger.exception("Error during project update using keystone: {}".format(e))
485 raise AuthconnOperationException("Error during project update using Keystone: {}".format(e
))
487 def assign_role_to_user(self
, user
, project
, role
):
489 Assigning a role to a user in a project.
491 :param user: username.
492 :param project: project name.
493 :param role: role name.
494 :raises AuthconnOperationException: if role assignment failed.
497 if is_valid_uuid(user
):
498 user_obj
= self
.keystone
.users
.get(user
)
500 user_obj_list
= self
.keystone
.users
.list(name
=user
)
501 if not user_obj_list
:
502 raise AuthconnNotFoundException("User '{}' not found".format(user
))
503 user_obj
= user_obj_list
[0]
505 if is_valid_uuid(project
):
506 project_obj
= self
.keystone
.projects
.get(project
)
508 project_obj_list
= self
.keystone
.projects
.list(name
=project
)
509 if not project_obj_list
:
510 raise AuthconnNotFoundException("Project '{}' not found".format(project
))
511 project_obj
= project_obj_list
[0]
513 if is_valid_uuid(role
):
514 role_obj
= self
.keystone
.roles
.get(role
)
516 role_obj_list
= self
.keystone
.roles
.list(name
=role
)
517 if not role_obj_list
:
518 raise AuthconnNotFoundException("Role '{}' not found".format(role
))
519 role_obj
= role_obj_list
[0]
521 self
.keystone
.roles
.grant(role_obj
, user
=user_obj
, project
=project_obj
)
522 except ClientException
as e
:
523 # self.logger.exception("Error during user role assignment using keystone: {}".format(e))
524 raise AuthconnOperationException("Error during role '{}' assignment to user '{}' and project '{}' using "
525 "Keystone: {}".format(role
, user
, project
, e
))
527 def remove_role_from_user(self
, user
, project
, role
):
529 Remove a role from a user in a project.
531 :param user: username.
532 :param project: project name or id.
533 :param role: role name or id.
535 :raises AuthconnOperationException: if role assignment revocation failed.
538 if is_valid_uuid(user
):
539 user_obj
= self
.keystone
.users
.get(user
)
541 user_obj_list
= self
.keystone
.users
.list(name
=user
)
542 if not user_obj_list
:
543 raise AuthconnNotFoundException("User '{}' not found".format(user
))
544 user_obj
= user_obj_list
[0]
546 if is_valid_uuid(project
):
547 project_obj
= self
.keystone
.projects
.get(project
)
549 project_obj_list
= self
.keystone
.projects
.list(name
=project
)
550 if not project_obj_list
:
551 raise AuthconnNotFoundException("Project '{}' not found".format(project
))
552 project_obj
= project_obj_list
[0]
554 if is_valid_uuid(role
):
555 role_obj
= self
.keystone
.roles
.get(role
)
557 role_obj_list
= self
.keystone
.roles
.list(name
=role
)
558 if not role_obj_list
:
559 raise AuthconnNotFoundException("Role '{}' not found".format(role
))
560 role_obj
= role_obj_list
[0]
562 self
.keystone
.roles
.revoke(role_obj
, user
=user_obj
, project
=project_obj
)
563 except ClientException
as e
:
564 # self.logger.exception("Error during user role revocation using keystone: {}".format(e))
565 raise AuthconnOperationException("Error during role '{}' revocation to user '{}' and project '{}' using "
566 "Keystone: {}".format(role
, user
, project
, e
))