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
):
48 Authconn
.__init
__(self
, config
)
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 authenticate_with_token(self, token, project=None):
162 # Authenticate a user using a token. Can be used to revalidate the token
163 # or to get a scoped token.
165 # :param token: a valid token.
166 # :param project: (optional) project for a scoped token.
167 # :return: return a revalidated token, scoped if a project was passed or
168 # the previous token was already scoped.
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]
175 # new_token = self.keystone.get_raw_token_from_identity_service(
176 # auth_url=self.auth_url,
178 # project_name=project,
180 # user_domain_name=self.user_domain_name,
181 # project_domain_name=self.project_domain_name)
183 # return new_token["auth_token"], project_names
184 # except ClientException as e:
185 # # self.logger.exception("Error during user authentication using keystone. Method: bearer: {}".format(e))
186 # raise AuthException("Error during user authentication using Keystone: {}".format(e),
187 # http_code=HTTPStatus.UNAUTHORIZED)
189 def validate_token(self
, token
):
191 Check if the token is valid.
193 :param token: token id to be validated
194 :return: dictionary with information associated with the token:
197 "project_id": project_id,
199 "roles": list with dict containing {name, id}
200 If the token is not valid an exception is raised.
206 token_info
= self
.keystone
.tokens
.validate(token
=token
)
208 "_id": token_info
["auth_token"],
209 "id": token_info
["auth_token"],
210 "project_id": token_info
["project"]["id"],
211 "project_name": token_info
["project"]["name"],
212 "user_id": token_info
["user"]["id"],
213 "username": token_info
["user"]["name"],
214 "roles": token_info
["roles"],
215 "expires": token_info
.expires
.timestamp(),
216 "issued_at": token_info
.issued
.timestamp()
220 except ClientException
as e
:
221 # self.logger.exception("Error during token validation using keystone: {}".format(e))
222 raise AuthException("Error during token validation using Keystone: {}".format(e
),
223 http_code
=HTTPStatus
.UNAUTHORIZED
)
225 def revoke_token(self
, token
):
229 :param token: token to be revoked
232 self
.logger
.info("Revoking token: " + token
)
233 self
.keystone
.tokens
.revoke_token(token
=token
)
236 except ClientException
as e
:
237 # self.logger.exception("Error during token revocation using keystone: {}".format(e))
238 raise AuthException("Error during token revocation using Keystone: {}".format(e
),
239 http_code
=HTTPStatus
.UNAUTHORIZED
)
241 def get_user_project_list(self
, token
):
243 Get all the projects associated with a user.
245 :param token: valid token
246 :return: list of projects
249 token_info
= self
.keystone
.tokens
.validate(token
=token
)
250 projects
= self
.keystone
.projects
.list(user
=token_info
["user"]["id"])
251 project_names
= [project
.name
for project
in projects
]
254 except ClientException
as e
:
255 # self.logger.exception("Error during user project listing using keystone: {}".format(e))
256 raise AuthException("Error during user project listing using Keystone: {}".format(e
),
257 http_code
=HTTPStatus
.UNAUTHORIZED
)
259 def get_user_role_list(self
, token
):
261 Get role list for a scoped project.
263 :param token: scoped token.
264 :return: returns the list of roles for the user in that project. If
265 the token is unscoped it returns None.
268 token_info
= self
.keystone
.tokens
.validate(token
=token
)
269 roles_info
= self
.keystone
.roles
.list(user
=token_info
["user"]["id"], project
=token_info
["project"]["id"])
271 roles
= [role
.name
for role
in roles_info
]
274 except ClientException
as e
:
275 # self.logger.exception("Error during user role listing using keystone: {}".format(e))
276 raise AuthException("Error during user role listing using Keystone: {}".format(e
),
277 http_code
=HTTPStatus
.UNAUTHORIZED
)
279 def create_user(self
, user
, password
):
283 :param user: username.
284 :param password: password.
285 :raises AuthconnOperationException: if user creation failed.
286 :return: returns the id of the user in keystone.
289 new_user
= self
.keystone
.users
.create(user
, password
=password
, domain
=self
.user_domain_name
)
290 return {"username": new_user
.name
, "_id": new_user
.id}
291 except Conflict
as e
:
292 # self.logger.exception("Error during user creation using keystone: {}".format(e))
293 raise AuthconnOperationException(e
, http_code
=HTTPStatus
.CONFLICT
)
294 except ClientException
as e
:
295 # self.logger.exception("Error during user creation using keystone: {}".format(e))
296 raise AuthconnOperationException("Error during user creation using Keystone: {}".format(e
))
298 def update_user(self
, user
, new_name
=None, new_password
=None):
300 Change the user name and/or password.
302 :param user: username or user_id
303 :param new_name: new name
304 :param new_password: new password.
305 :raises AuthconnOperationException: if change failed.
308 if is_valid_uuid(user
):
311 user_obj_list
= self
.keystone
.users
.list(name
=user
)
312 if not user_obj_list
:
313 raise AuthconnNotFoundException("User '{}' not found".format(user
))
314 user_id
= user_obj_list
[0].id
316 self
.keystone
.users
.update(user_id
, password
=new_password
, name
=new_name
)
317 except ClientException
as e
:
318 # self.logger.exception("Error during user password/name update using keystone: {}".format(e))
319 raise AuthconnOperationException("Error during user password/name update using Keystone: {}".format(e
))
321 def delete_user(self
, user_id
):
325 :param user_id: user identifier.
326 :raises AuthconnOperationException: if user deletion failed.
329 # users = self.keystone.users.list()
330 # user_obj = [user for user in users if user.id == user_id][0]
331 # result, _ = self.keystone.users.delete(user_obj)
333 result
, detail
= self
.keystone
.users
.delete(user_id
)
334 if result
.status_code
!= 204:
335 raise ClientException("error {} {}".format(result
.status_code
, detail
))
338 except ClientException
as e
:
339 # self.logger.exception("Error during user deletion using keystone: {}".format(e))
340 raise AuthconnOperationException("Error during user deletion using Keystone: {}".format(e
))
342 def get_user_list(self
, filter_q
=None):
346 :param filter_q: dictionary to filter user list by name (username is also admited) and/or _id
347 :return: returns a list of users.
352 filter_name
= filter_q
.get("name") or filter_q
.get("username")
353 users
= self
.keystone
.users
.list(name
=filter_name
)
355 "username": user
.name
,
358 } for user
in users
if user
.name
!= self
.admin_username
]
360 if filter_q
and filter_q
.get("_id"):
361 users
= [user
for user
in users
if filter_q
["_id"] == user
["_id"]]
364 projects
= self
.keystone
.projects
.list(user
=user
["_id"])
366 "name": project
.name
,
369 } for project
in projects
]
371 for project
in projects
:
372 roles
= self
.keystone
.roles
.list(user
=user
["_id"], project
=project
["_id"])
378 project
["roles"] = roles
380 user
["projects"] = projects
383 except ClientException
as e
:
384 # self.logger.exception("Error during user listing using keystone: {}".format(e))
385 raise AuthconnOperationException("Error during user listing using Keystone: {}".format(e
))
387 def get_role_list(self
, filter_q
=None):
391 :param filter_q: dictionary to filter role list by _id and/or name.
392 :return: returns the list of roles.
397 filter_name
= filter_q
.get("name")
398 roles_list
= self
.keystone
.roles
.list(name
=filter_name
)
403 } for role
in roles_list
if role
.name
!= "service"]
405 if filter_q
and filter_q
.get("_id"):
406 roles
= [role
for role
in roles
if filter_q
["_id"] == role
["_id"]]
409 except ClientException
as e
:
410 # self.logger.exception("Error during user role listing using keystone: {}".format(e))
411 raise AuthException("Error during user role listing using Keystone: {}".format(e
),
412 http_code
=HTTPStatus
.UNAUTHORIZED
)
414 def create_role(self
, role
):
418 :param role: role name.
419 :raises AuthconnOperationException: if role creation failed.
422 result
= self
.keystone
.roles
.create(role
)
424 except Conflict
as ex
:
425 raise AuthconnConflictException(str(ex
))
426 except ClientException
as e
:
427 # self.logger.exception("Error during role creation using keystone: {}".format(e))
428 raise AuthconnOperationException("Error during role creation using Keystone: {}".format(e
))
430 def delete_role(self
, role_id
):
434 :param role_id: role identifier.
435 :raises AuthconnOperationException: if role deletion failed.
438 result
, detail
= self
.keystone
.roles
.delete(role_id
)
440 if result
.status_code
!= 204:
441 raise ClientException("error {} {}".format(result
.status_code
, detail
))
444 except ClientException
as e
:
445 # self.logger.exception("Error during role deletion using keystone: {}".format(e))
446 raise AuthconnOperationException("Error during role deletion using Keystone: {}".format(e
))
448 def update_role(self
, role
, new_name
):
450 Change the name of a role
451 :param role: role name or id to be changed
452 :param new_name: new name
456 if is_valid_uuid(role
):
459 role_obj_list
= self
.keystone
.roles
.list(name
=role
)
460 if not role_obj_list
:
461 raise AuthconnNotFoundException("Role '{}' not found".format(role
))
462 role_id
= role_obj_list
[0].id
463 self
.keystone
.roles
.update(role_id
, name
=new_name
)
464 except ClientException
as e
:
465 # self.logger.exception("Error during role update using keystone: {}".format(e))
466 raise AuthconnOperationException("Error during role updating using Keystone: {}".format(e
))
468 def get_project_list(self
, filter_q
=None):
470 Get all the projects.
472 :param filter_q: dictionary to filter project list.
473 :return: list of projects
478 filter_name
= filter_q
.get("name")
479 projects
= self
.keystone
.projects
.list(name
=filter_name
)
482 "name": project
.name
,
484 } for project
in projects
]
486 if filter_q
and filter_q
.get("_id"):
487 projects
= [project
for project
in projects
488 if filter_q
["_id"] == project
["_id"]]
491 except ClientException
as e
:
492 # self.logger.exception("Error during user project listing using keystone: {}".format(e))
493 raise AuthException("Error during user project listing using Keystone: {}".format(e
),
494 http_code
=HTTPStatus
.UNAUTHORIZED
)
496 def create_project(self
, project
):
500 :param project: project name.
501 :return: the internal id of the created project
502 :raises AuthconnOperationException: if project creation failed.
505 result
= self
.keystone
.projects
.create(project
, self
.project_domain_name
)
507 except ClientException
as e
:
508 # self.logger.exception("Error during project creation using keystone: {}".format(e))
509 raise AuthconnOperationException("Error during project creation using Keystone: {}".format(e
))
511 def delete_project(self
, project_id
):
515 :param project_id: project identifier.
516 :raises AuthconnOperationException: if project deletion failed.
519 # projects = self.keystone.projects.list()
520 # project_obj = [project for project in projects if project.id == project_id][0]
521 # result, _ = self.keystone.projects.delete(project_obj)
523 result
, detail
= self
.keystone
.projects
.delete(project_id
)
524 if result
.status_code
!= 204:
525 raise ClientException("error {} {}".format(result
.status_code
, detail
))
528 except ClientException
as e
:
529 # self.logger.exception("Error during project deletion using keystone: {}".format(e))
530 raise AuthconnOperationException("Error during project deletion using Keystone: {}".format(e
))
532 def update_project(self
, project_id
, new_name
):
534 Change the name of a project
535 :param project_id: project to be changed
536 :param new_name: new name
540 self
.keystone
.projects
.update(project_id
, name
=new_name
)
541 except ClientException
as e
:
542 # self.logger.exception("Error during project update using keystone: {}".format(e))
543 raise AuthconnOperationException("Error during project deletion using Keystone: {}".format(e
))
545 def assign_role_to_user(self
, user
, project
, role
):
547 Assigning a role to a user in a project.
549 :param user: username.
550 :param project: project name.
551 :param role: role name.
552 :raises AuthconnOperationException: if role assignment failed.
555 if is_valid_uuid(user
):
556 user_obj
= self
.keystone
.users
.get(user
)
558 user_obj_list
= self
.keystone
.users
.list(name
=user
)
559 if not user_obj_list
:
560 raise AuthconnNotFoundException("User '{}' not found".format(user
))
561 user_obj
= user_obj_list
[0]
563 if is_valid_uuid(project
):
564 project_obj
= self
.keystone
.projects
.get(project
)
566 project_obj_list
= self
.keystone
.projects
.list(name
=project
)
567 if not project_obj_list
:
568 raise AuthconnNotFoundException("Project '{}' not found".format(project
))
569 project_obj
= project_obj_list
[0]
571 if is_valid_uuid(role
):
572 role_obj
= self
.keystone
.roles
.get(role
)
574 role_obj_list
= self
.keystone
.roles
.list(name
=role
)
575 if not role_obj_list
:
576 raise AuthconnNotFoundException("Role '{}' not found".format(role
))
577 role_obj
= role_obj_list
[0]
579 self
.keystone
.roles
.grant(role_obj
, user
=user_obj
, project
=project_obj
)
580 except ClientException
as e
:
581 # self.logger.exception("Error during user role assignment using keystone: {}".format(e))
582 raise AuthconnOperationException("Error during role '{}' assignment to user '{}' and project '{}' using "
583 "Keystone: {}".format(role
, user
, project
, e
))
585 def remove_role_from_user(self
, user
, project
, role
):
587 Remove a role from a user in a project.
589 :param user: username.
590 :param project: project name or id.
591 :param role: role name or id.
593 :raises AuthconnOperationException: if role assignment revocation failed.
596 if is_valid_uuid(user
):
597 user_obj
= self
.keystone
.users
.get(user
)
599 user_obj_list
= self
.keystone
.users
.list(name
=user
)
600 if not user_obj_list
:
601 raise AuthconnNotFoundException("User '{}' not found".format(user
))
602 user_obj
= user_obj_list
[0]
604 if is_valid_uuid(project
):
605 project_obj
= self
.keystone
.projects
.get(project
)
607 project_obj_list
= self
.keystone
.projects
.list(name
=project
)
608 if not project_obj_list
:
609 raise AuthconnNotFoundException("Project '{}' not found".format(project
))
610 project_obj
= project_obj_list
[0]
612 if is_valid_uuid(role
):
613 role_obj
= self
.keystone
.roles
.get(role
)
615 role_obj_list
= self
.keystone
.roles
.list(name
=role
)
616 if not role_obj_list
:
617 raise AuthconnNotFoundException("Role '{}' not found".format(role
))
618 role_obj
= role_obj_list
[0]
620 self
.keystone
.roles
.revoke(role_obj
, user
=user_obj
, project
=project_obj
)
621 except ClientException
as e
:
622 # self.logger.exception("Error during user role revocation using keystone: {}".format(e))
623 raise AuthconnOperationException("Error during role '{}' revocation to user '{}' and project '{}' using "
624 "Keystone: {}".format(role
, user
, project
, e
))