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
=None):
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:
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
)
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 "username": scoped_token
.username
,
146 "project_id": scoped_token
.project_id
,
147 "project_name": scoped_token
.project_name
,
148 "expires": scoped_token
.expires
.timestamp(),
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
)
157 # def authenticate_with_token(self, token, project=None):
159 # Authenticate a user using a token. Can be used to revalidate the token
160 # or to get a scoped token.
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.
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]
172 # new_token = self.keystone.get_raw_token_from_identity_service(
173 # auth_url=self.auth_url,
175 # project_name=project,
177 # user_domain_name=self.user_domain_name,
178 # project_domain_name=self.project_domain_name)
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)
186 def validate_token(self
, token
):
188 Check if the token is valid.
190 :param token: token to validate
191 :return: dictionary with information associated with the token. If the
192 token is not valid, returns None.
198 token_info
= self
.keystone
.tokens
.validate(token
=token
)
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
)
206 def revoke_token(self
, token
):
210 :param token: token to be revoked
213 self
.logger
.info("Revoking token: " + token
)
214 self
.keystone
.tokens
.revoke_token(token
=token
)
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
)
222 def get_user_project_list(self
, token
):
224 Get all the projects associated with a user.
226 :param token: valid token
227 :return: list of projects
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
]
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
)
240 def get_user_role_list(self
, token
):
242 Get role list for a scoped project.
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.
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"])
252 roles
= [role
.name
for role
in roles_info
]
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
)
260 def create_user(self
, user
, password
):
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.
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
))
279 def update_user(self
, user
, new_name
=None, new_password
=None):
281 Change the user name and/or password.
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.
289 if is_valid_uuid(user
):
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
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
))
302 def delete_user(self
, user_id
):
306 :param user_id: user identifier.
307 :raises AuthconnOperationException: if user deletion failed.
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)
314 result
, detail
= self
.keystone
.users
.delete(user_id
)
315 if result
.status_code
!= 204:
316 raise ClientException("error {} {}".format(result
.status_code
, detail
))
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
))
323 def get_user_list(self
, filter_q
=None):
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.
333 filter_name
= filter_q
.get("name") or filter_q
.get("username")
334 users
= self
.keystone
.users
.list(name
=filter_name
)
336 "username": user
.name
,
339 } for user
in users
if user
.name
!= self
.admin_username
]
341 if filter_q
and filter_q
.get("_id"):
342 users
= [user
for user
in users
if filter_q
["_id"] == user
["_id"]]
345 projects
= self
.keystone
.projects
.list(user
=user
["_id"])
347 "name": project
.name
,
350 } for project
in projects
]
352 for project
in projects
:
353 roles
= self
.keystone
.roles
.list(user
=user
["_id"], project
=project
["_id"])
359 project
["roles"] = roles
361 user
["projects"] = projects
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
))
368 def get_role_list(self
, filter_q
=None):
372 :param filter_q: dictionary to filter role list by _id and/or name.
373 :return: returns the list of roles.
378 filter_name
= filter_q
.get("name")
379 roles_list
= self
.keystone
.roles
.list(name
=filter_name
)
384 } for role
in roles_list
if role
.name
!= "service"]
386 if filter_q
and filter_q
.get("_id"):
387 roles
= [role
for role
in roles
if filter_q
["_id"] == role
["_id"]]
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
)
395 def create_role(self
, role
):
399 :param role: role name.
400 :raises AuthconnOperationException: if role creation failed.
403 result
= self
.keystone
.roles
.create(role
)
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
))
411 def delete_role(self
, role_id
):
415 :param role_id: role identifier.
416 :raises AuthconnOperationException: if role deletion failed.
419 result
, detail
= self
.keystone
.roles
.delete(role_id
)
421 if result
.status_code
!= 204:
422 raise ClientException("error {} {}".format(result
.status_code
, detail
))
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
))
429 def update_role(self
, role
, new_name
):
431 Change the name of a role
432 :param role: role name or id to be changed
433 :param new_name: new name
437 if is_valid_uuid(role
):
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
))
449 def get_project_list(self
, filter_q
=None):
451 Get all the projects.
453 :param filter_q: dictionary to filter project list.
454 :return: list of projects
459 filter_name
= filter_q
.get("name")
460 projects
= self
.keystone
.projects
.list(name
=filter_name
)
463 "name": project
.name
,
465 } for project
in projects
]
467 if filter_q
and filter_q
.get("_id"):
468 projects
= [project
for project
in projects
469 if filter_q
["_id"] == project
["_id"]]
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
)
477 def create_project(self
, project
):
481 :param project: project name.
482 :return: the internal id of the created project
483 :raises AuthconnOperationException: if project creation failed.
486 result
= self
.keystone
.projects
.create(project
, self
.project_domain_name
)
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
))
492 def delete_project(self
, project_id
):
496 :param project_id: project identifier.
497 :raises AuthconnOperationException: if project deletion failed.
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)
504 result
, detail
= self
.keystone
.projects
.delete(project_id
)
505 if result
.status_code
!= 204:
506 raise ClientException("error {} {}".format(result
.status_code
, detail
))
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
))
513 def update_project(self
, project_id
, new_name
):
515 Change the name of a project
516 :param project_id: project to be changed
517 :param new_name: new name
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
))
526 def assign_role_to_user(self
, user
, project
, role
):
528 Assigning a role to a user in a project.
530 :param user: username.
531 :param project: project name.
532 :param role: role name.
533 :raises AuthconnOperationException: if role assignment failed.
536 if is_valid_uuid(user
):
537 user_obj
= self
.keystone
.users
.get(user
)
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]
544 if is_valid_uuid(project
):
545 project_obj
= self
.keystone
.projects
.get(project
)
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]
552 if is_valid_uuid(role
):
553 role_obj
= self
.keystone
.roles
.get(role
)
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]
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
))
566 def remove_role_from_user(self
, user
, project
, role
):
568 Remove a role from a user in a project.
570 :param user: username.
571 :param project: project name or id.
572 :param role: role name or id.
574 :raises AuthconnOperationException: if role assignment revocation failed.
577 if is_valid_uuid(user
):
578 user_obj
= self
.keystone
.users
.get(user
)
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]
585 if is_valid_uuid(project
):
586 project_obj
= self
.keystone
.projects
.get(project
)
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]
593 if is_valid_uuid(role
):
594 role_obj
= self
.keystone
.roles
.get(role
)
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]
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
))