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:
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 "project_id": token_info
["project"]["id"],
207 "project_name": token_info
["project"]["name"],
208 "user_id": token_info
["user"]["id"],
209 "username": token_info
["user"]["name"],
210 "roles": token_info
["roles"],
211 "expires": token_info
.expires
.timestamp()
215 except ClientException
as e
:
216 self
.logger
.exception("Error during token validation using keystone: {}".format(e
))
217 raise AuthException("Error during token validation using Keystone: {}".format(e
),
218 http_code
=HTTPStatus
.UNAUTHORIZED
)
220 def revoke_token(self
, token
):
224 :param token: token to be revoked
227 self
.logger
.info("Revoking token: " + token
)
228 self
.keystone
.tokens
.revoke_token(token
=token
)
231 except ClientException
as e
:
232 self
.logger
.exception("Error during token revocation using keystone: {}".format(e
))
233 raise AuthException("Error during token revocation using Keystone: {}".format(e
),
234 http_code
=HTTPStatus
.UNAUTHORIZED
)
236 def get_user_project_list(self
, token
):
238 Get all the projects associated with a user.
240 :param token: valid token
241 :return: list of projects
244 token_info
= self
.keystone
.tokens
.validate(token
=token
)
245 projects
= self
.keystone
.projects
.list(user
=token_info
["user"]["id"])
246 project_names
= [project
.name
for project
in projects
]
249 except ClientException
as e
:
250 self
.logger
.exception("Error during user project listing using keystone: {}".format(e
))
251 raise AuthException("Error during user project listing using Keystone: {}".format(e
),
252 http_code
=HTTPStatus
.UNAUTHORIZED
)
254 def get_user_role_list(self
, token
):
256 Get role list for a scoped project.
258 :param token: scoped token.
259 :return: returns the list of roles for the user in that project. If
260 the token is unscoped it returns None.
263 token_info
= self
.keystone
.tokens
.validate(token
=token
)
264 roles_info
= self
.keystone
.roles
.list(user
=token_info
["user"]["id"], project
=token_info
["project"]["id"])
266 roles
= [role
.name
for role
in roles_info
]
269 except ClientException
as e
:
270 self
.logger
.exception("Error during user role listing using keystone: {}".format(e
))
271 raise AuthException("Error during user role listing using Keystone: {}".format(e
),
272 http_code
=HTTPStatus
.UNAUTHORIZED
)
274 def create_user(self
, user
, password
):
278 :param user: username.
279 :param password: password.
280 :raises AuthconnOperationException: if user creation failed.
281 :return: returns the id of the user in keystone.
284 new_user
= self
.keystone
.users
.create(user
, password
=password
, domain
=self
.user_domain_name
)
285 return {"username": new_user
.name
, "_id": new_user
.id}
286 except Conflict
as e
:
287 # self.logger.exception("Error during user creation using keystone: {}".format(e))
288 raise AuthconnOperationException(e
, http_code
=HTTPStatus
.CONFLICT
)
289 except ClientException
as e
:
290 self
.logger
.exception("Error during user creation using keystone: {}".format(e
))
291 raise AuthconnOperationException("Error during user creation using Keystone: {}".format(e
))
293 def update_user(self
, user
, new_name
=None, new_password
=None):
295 Change the user name and/or password.
297 :param user: username or user_id
298 :param new_name: new name
299 :param new_password: new password.
300 :raises AuthconnOperationException: if change failed.
303 if is_valid_uuid(user
):
306 user_obj_list
= self
.keystone
.users
.list(name
=user
)
307 if not user_obj_list
:
308 raise AuthconnNotFoundException("User '{}' not found".format(user
))
309 user_id
= user_obj_list
[0].id
311 self
.keystone
.users
.update(user_id
, password
=new_password
, name
=new_name
)
312 except ClientException
as e
:
313 self
.logger
.exception("Error during user password/name update using keystone: {}".format(e
))
314 raise AuthconnOperationException("Error during user password/name update using Keystone: {}".format(e
))
316 def delete_user(self
, user_id
):
320 :param user_id: user identifier.
321 :raises AuthconnOperationException: if user deletion failed.
324 # users = self.keystone.users.list()
325 # user_obj = [user for user in users if user.id == user_id][0]
326 # result, _ = self.keystone.users.delete(user_obj)
328 result
, detail
= self
.keystone
.users
.delete(user_id
)
329 if result
.status_code
!= 204:
330 raise ClientException("error {} {}".format(result
.status_code
, detail
))
333 except ClientException
as e
:
334 self
.logger
.exception("Error during user deletion using keystone: {}".format(e
))
335 raise AuthconnOperationException("Error during user deletion using Keystone: {}".format(e
))
337 def get_user_list(self
, filter_q
=None):
341 :param filter_q: dictionary to filter user list by name (username is also admited) and/or _id
342 :return: returns a list of users.
347 filter_name
= filter_q
.get("name") or filter_q
.get("username")
348 users
= self
.keystone
.users
.list(name
=filter_name
)
350 "username": user
.name
,
353 } for user
in users
if user
.name
!= self
.admin_username
]
355 if filter_q
and filter_q
.get("_id"):
356 users
= [user
for user
in users
if filter_q
["_id"] == user
["_id"]]
359 projects
= self
.keystone
.projects
.list(user
=user
["_id"])
361 "name": project
.name
,
364 } for project
in projects
]
366 for project
in projects
:
367 roles
= self
.keystone
.roles
.list(user
=user
["_id"], project
=project
["_id"])
373 project
["roles"] = roles
375 user
["projects"] = projects
378 except ClientException
as e
:
379 self
.logger
.exception("Error during user listing using keystone: {}".format(e
))
380 raise AuthconnOperationException("Error during user listing using Keystone: {}".format(e
))
382 def get_role_list(self
, filter_q
=None):
386 :param filter_q: dictionary to filter role list by _id and/or name.
387 :return: returns the list of roles.
392 filter_name
= filter_q
.get("name")
393 roles_list
= self
.keystone
.roles
.list(name
=filter_name
)
398 } for role
in roles_list
if role
.name
!= "service"]
400 if filter_q
and filter_q
.get("_id"):
401 roles
= [role
for role
in roles
if filter_q
["_id"] == role
["_id"]]
404 except ClientException
as e
:
405 self
.logger
.exception("Error during user role listing using keystone: {}".format(e
))
406 raise AuthException("Error during user role listing using Keystone: {}".format(e
),
407 http_code
=HTTPStatus
.UNAUTHORIZED
)
409 def create_role(self
, role
):
413 :param role: role name.
414 :raises AuthconnOperationException: if role creation failed.
417 result
= self
.keystone
.roles
.create(role
)
419 except Conflict
as ex
:
420 raise AuthconnConflictException(str(ex
))
421 except ClientException
as e
:
422 self
.logger
.exception("Error during role creation using keystone: {}".format(e
))
423 raise AuthconnOperationException("Error during role creation using Keystone: {}".format(e
))
425 def delete_role(self
, role_id
):
429 :param role_id: role identifier.
430 :raises AuthconnOperationException: if role deletion failed.
433 result
, detail
= self
.keystone
.roles
.delete(role_id
)
435 if result
.status_code
!= 204:
436 raise ClientException("error {} {}".format(result
.status_code
, detail
))
439 except ClientException
as e
:
440 self
.logger
.exception("Error during role deletion using keystone: {}".format(e
))
441 raise AuthconnOperationException("Error during role deletion using Keystone: {}".format(e
))
443 def update_role(self
, role
, new_name
):
445 Change the name of a role
446 :param role: role name or id to be changed
447 :param new_name: new name
451 if is_valid_uuid(role
):
454 role_obj_list
= self
.keystone
.roles
.list(name
=role
)
455 if not role_obj_list
:
456 raise AuthconnNotFoundException("Role '{}' not found".format(role
))
457 role_id
= role_obj_list
[0].id
458 self
.keystone
.roles
.update(role_id
, name
=new_name
)
459 except ClientException
as e
:
460 # self.logger.exception("Error during role update using keystone: {}".format(e))
461 raise AuthconnOperationException("Error during role updating using Keystone: {}".format(e
))
463 def get_project_list(self
, filter_q
=None):
465 Get all the projects.
467 :param filter_q: dictionary to filter project list.
468 :return: list of projects
473 filter_name
= filter_q
.get("name")
474 projects
= self
.keystone
.projects
.list(name
=filter_name
)
477 "name": project
.name
,
479 } for project
in projects
]
481 if filter_q
and filter_q
.get("_id"):
482 projects
= [project
for project
in projects
483 if filter_q
["_id"] == project
["_id"]]
486 except ClientException
as e
:
487 self
.logger
.exception("Error during user project listing using keystone: {}".format(e
))
488 raise AuthException("Error during user project listing using Keystone: {}".format(e
),
489 http_code
=HTTPStatus
.UNAUTHORIZED
)
491 def create_project(self
, project
):
495 :param project: project name.
496 :return: the internal id of the created project
497 :raises AuthconnOperationException: if project creation failed.
500 result
= self
.keystone
.projects
.create(project
, self
.project_domain_name
)
502 except ClientException
as e
:
503 self
.logger
.exception("Error during project creation using keystone: {}".format(e
))
504 raise AuthconnOperationException("Error during project creation using Keystone: {}".format(e
))
506 def delete_project(self
, project_id
):
510 :param project_id: project identifier.
511 :raises AuthconnOperationException: if project deletion failed.
514 # projects = self.keystone.projects.list()
515 # project_obj = [project for project in projects if project.id == project_id][0]
516 # result, _ = self.keystone.projects.delete(project_obj)
518 result
, detail
= self
.keystone
.projects
.delete(project_id
)
519 if result
.status_code
!= 204:
520 raise ClientException("error {} {}".format(result
.status_code
, detail
))
523 except ClientException
as e
:
524 self
.logger
.exception("Error during project deletion using keystone: {}".format(e
))
525 raise AuthconnOperationException("Error during project deletion using Keystone: {}".format(e
))
527 def update_project(self
, project_id
, new_name
):
529 Change the name of a project
530 :param project_id: project to be changed
531 :param new_name: new name
535 self
.keystone
.projects
.update(project_id
, name
=new_name
)
536 except ClientException
as e
:
537 self
.logger
.exception("Error during project update using keystone: {}".format(e
))
538 raise AuthconnOperationException("Error during project deletion using Keystone: {}".format(e
))
540 def assign_role_to_user(self
, user
, project
, role
):
542 Assigning a role to a user in a project.
544 :param user: username.
545 :param project: project name.
546 :param role: role name.
547 :raises AuthconnOperationException: if role assignment failed.
550 if is_valid_uuid(user
):
551 user_obj
= self
.keystone
.users
.get(user
)
553 user_obj_list
= self
.keystone
.users
.list(name
=user
)
554 if not user_obj_list
:
555 raise AuthconnNotFoundException("User '{}' not found".format(user
))
556 user_obj
= user_obj_list
[0]
558 if is_valid_uuid(project
):
559 project_obj
= self
.keystone
.projects
.get(project
)
561 project_obj_list
= self
.keystone
.projects
.list(name
=project
)
562 if not project_obj_list
:
563 raise AuthconnNotFoundException("Project '{}' not found".format(project
))
564 project_obj
= project_obj_list
[0]
566 if is_valid_uuid(role
):
567 role_obj
= self
.keystone
.roles
.get(role
)
569 role_obj_list
= self
.keystone
.roles
.list(name
=role
)
570 if not role_obj_list
:
571 raise AuthconnNotFoundException("Role '{}' not found".format(role
))
572 role_obj
= role_obj_list
[0]
574 self
.keystone
.roles
.grant(role_obj
, user
=user_obj
, project
=project_obj
)
575 except ClientException
as e
:
576 self
.logger
.exception("Error during user role assignment using keystone: {}".format(e
))
577 raise AuthconnOperationException("Error during role '{}' assignment to user '{}' and project '{}' using "
578 "Keystone: {}".format(role
, user
, project
, e
))
580 def remove_role_from_user(self
, user
, project
, role
):
582 Remove a role from a user in a project.
584 :param user: username.
585 :param project: project name or id.
586 :param role: role name or id.
588 :raises AuthconnOperationException: if role assignment revocation failed.
591 if is_valid_uuid(user
):
592 user_obj
= self
.keystone
.users
.get(user
)
594 user_obj_list
= self
.keystone
.users
.list(name
=user
)
595 if not user_obj_list
:
596 raise AuthconnNotFoundException("User '{}' not found".format(user
))
597 user_obj
= user_obj_list
[0]
599 if is_valid_uuid(project
):
600 project_obj
= self
.keystone
.projects
.get(project
)
602 project_obj_list
= self
.keystone
.projects
.list(name
=project
)
603 if not project_obj_list
:
604 raise AuthconnNotFoundException("Project '{}' not found".format(project
))
605 project_obj
= project_obj_list
[0]
607 if is_valid_uuid(role
):
608 role_obj
= self
.keystone
.roles
.get(role
)
610 role_obj_list
= self
.keystone
.roles
.list(name
=role
)
611 if not role_obj_list
:
612 raise AuthconnNotFoundException("Role '{}' not found".format(role
))
613 role_obj
= role_obj_list
[0]
615 self
.keystone
.roles
.revoke(role_obj
, user
=user_obj
, project
=project_obj
)
616 except ClientException
as e
:
617 self
.logger
.exception("Error during user role revocation using keystone: {}".format(e
))
618 raise AuthconnOperationException("Error during role '{}' revocation to user '{}' and project '{}' using "
619 "Keystone: {}".format(role
, user
, project
, e
))