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
36 from keystoneauth1
import session
37 from keystoneauth1
.identity
import v3
38 from keystoneauth1
.exceptions
.base
import ClientException
39 from keystoneauth1
.exceptions
.http
import Conflict
40 from keystoneclient
.v3
import client
41 from http
import HTTPStatus
42 from validation
import is_valid_uuid
45 class AuthconnKeystone(Authconn
):
46 def __init__(self
, config
):
47 Authconn
.__init
__(self
, config
)
49 self
.logger
= logging
.getLogger("nbi.authenticator.keystone")
51 self
.auth_url
= "http://{0}:{1}/v3".format(config
.get("auth_url", "keystone"), config
.get("auth_port", "5000"))
52 self
.user_domain_name
= config
.get("user_domain_name", "default")
53 self
.admin_project
= config
.get("service_project", "service")
54 self
.admin_username
= config
.get("service_username", "nbi")
55 self
.admin_password
= config
.get("service_password", "nbi")
56 self
.project_domain_name
= config
.get("project_domain_name", "default")
58 # Waiting for Keystone to be up
61 while available
is None:
64 result
= requests
.get(self
.auth_url
)
65 available
= True if result
.status_code
== 200 else None
69 raise AuthException("Keystone not available after 300s timeout")
71 self
.auth
= v3
.Password(user_domain_name
=self
.user_domain_name
,
72 username
=self
.admin_username
,
73 password
=self
.admin_password
,
74 project_domain_name
=self
.project_domain_name
,
75 project_name
=self
.admin_project
,
76 auth_url
=self
.auth_url
)
77 self
.sess
= session
.Session(auth
=self
.auth
)
78 self
.keystone
= client
.Client(session
=self
.sess
)
80 def authenticate(self
, user
, password
, project
=None, token
=None):
82 Authenticate a user using username/password or token, plus project
83 :param user: user: name, id or None
84 :param password: password or None
85 :param project: name, id, or None. If None first found project will be used to get an scope token
86 :param token: previous token to obtain authorization
87 :return: the scoped token info or raises an exception. The token is a dictionary with:
90 project_id: scoped_token project_id,
91 project_name: scoped_token project_name,
92 expires: epoch time when it expires,
102 if is_valid_uuid(user
):
107 # get an unscoped token firstly
108 unscoped_token
= self
.keystone
.get_raw_token_from_identity_service(
109 auth_url
=self
.auth_url
,
113 user_domain_name
=self
.user_domain_name
,
114 project_domain_name
=self
.project_domain_name
)
116 unscoped_token
= self
.keystone
.tokens
.validate(token
=token
)
118 raise AuthException("Provide credentials: username/password or Authorization Bearer token",
119 http_code
=HTTPStatus
.UNAUTHORIZED
)
122 # get first project for the user
123 project_list
= self
.keystone
.projects
.list(user
=unscoped_token
["user"]["id"])
125 raise AuthException("The user {} has not any project and cannot be used for authentication".
126 format(user
), http_code
=HTTPStatus
.UNAUTHORIZED
)
127 project_id
= project_list
[0].id
129 if is_valid_uuid(project
):
132 project_name
= project
134 scoped_token
= self
.keystone
.get_raw_token_from_identity_service(
135 auth_url
=self
.auth_url
,
136 project_name
=project_name
,
137 project_id
=project_id
,
138 user_domain_name
=self
.user_domain_name
,
139 project_domain_name
=self
.project_domain_name
,
140 token
=unscoped_token
["auth_token"])
143 "_id": scoped_token
.auth_token
,
144 "username": scoped_token
.username
,
145 "project_id": scoped_token
.project_id
,
146 "project_name": scoped_token
.project_name
,
147 "expires": scoped_token
.expires
.timestamp(),
151 except ClientException
as e
:
152 self
.logger
.exception("Error during user authentication using keystone. Method: basic: {}".format(e
))
153 raise AuthException("Error during user authentication using Keystone: {}".format(e
),
154 http_code
=HTTPStatus
.UNAUTHORIZED
)
156 # def authenticate_with_token(self, token, project=None):
158 # Authenticate a user using a token. Can be used to revalidate the token
159 # or to get a scoped token.
161 # :param token: a valid token.
162 # :param project: (optional) project for a scoped token.
163 # :return: return a revalidated token, scoped if a project was passed or
164 # the previous token was already scoped.
167 # token_info = self.keystone.tokens.validate(token=token)
168 # projects = self.keystone.projects.list(user=token_info["user"]["id"])
169 # project_names = [project.name for project in projects]
171 # new_token = self.keystone.get_raw_token_from_identity_service(
172 # auth_url=self.auth_url,
174 # project_name=project,
176 # user_domain_name=self.user_domain_name,
177 # project_domain_name=self.project_domain_name)
179 # return new_token["auth_token"], project_names
180 # except ClientException as e:
181 # self.logger.exception("Error during user authentication using keystone. Method: bearer: {}".format(e))
182 # raise AuthException("Error during user authentication using Keystone: {}".format(e),
183 # http_code=HTTPStatus.UNAUTHORIZED)
185 def validate_token(self
, token
):
187 Check if the token is valid.
189 :param token: token to validate
190 :return: dictionary with information associated with the token. If the
191 token is not valid, returns None.
197 token_info
= self
.keystone
.tokens
.validate(token
=token
)
200 except ClientException
as e
:
201 self
.logger
.exception("Error during token validation using keystone: {}".format(e
))
202 raise AuthException("Error during token validation using Keystone: {}".format(e
),
203 http_code
=HTTPStatus
.UNAUTHORIZED
)
205 def revoke_token(self
, token
):
209 :param token: token to be revoked
212 self
.logger
.info("Revoking token: " + token
)
213 self
.keystone
.tokens
.revoke_token(token
=token
)
216 except ClientException
as e
:
217 self
.logger
.exception("Error during token revocation using keystone: {}".format(e
))
218 raise AuthException("Error during token revocation using Keystone: {}".format(e
),
219 http_code
=HTTPStatus
.UNAUTHORIZED
)
221 def get_user_project_list(self
, token
):
223 Get all the projects associated with a user.
225 :param token: valid token
226 :return: list of projects
229 token_info
= self
.keystone
.tokens
.validate(token
=token
)
230 projects
= self
.keystone
.projects
.list(user
=token_info
["user"]["id"])
231 project_names
= [project
.name
for project
in projects
]
234 except ClientException
as e
:
235 self
.logger
.exception("Error during user project listing using keystone: {}".format(e
))
236 raise AuthException("Error during user project listing using Keystone: {}".format(e
),
237 http_code
=HTTPStatus
.UNAUTHORIZED
)
239 def get_user_role_list(self
, token
):
241 Get role list for a scoped project.
243 :param token: scoped token.
244 :return: returns the list of roles for the user in that project. If
245 the token is unscoped it returns None.
248 token_info
= self
.keystone
.tokens
.validate(token
=token
)
249 roles_info
= self
.keystone
.roles
.list(user
=token_info
["user"]["id"], project
=token_info
["project"]["id"])
251 roles
= [role
.name
for role
in roles_info
]
254 except ClientException
as e
:
255 self
.logger
.exception("Error during user role listing using keystone: {}".format(e
))
256 raise AuthException("Error during user role listing using Keystone: {}".format(e
),
257 http_code
=HTTPStatus
.UNAUTHORIZED
)
259 def create_user(self
, user
, password
):
263 :param user: username.
264 :param password: password.
265 :raises AuthconnOperationException: if user creation failed.
266 :return: returns the id of the user in keystone.
269 new_user
= self
.keystone
.users
.create(user
, password
=password
, domain
=self
.user_domain_name
)
270 return {"username": new_user
.name
, "_id": new_user
.id}
271 except Conflict
as e
:
272 # self.logger.exception("Error during user creation using keystone: {}".format(e))
273 raise AuthconnOperationException(e
, http_code
=HTTPStatus
.CONFLICT
)
274 except ClientException
as e
:
275 self
.logger
.exception("Error during user creation using keystone: {}".format(e
))
276 raise AuthconnOperationException("Error during user creation using Keystone: {}".format(e
))
278 def update_user(self
, user
, new_name
=None, new_password
=None):
280 Change the user name and/or password.
282 :param user: username or user_id
283 :param new_name: new name
284 :param new_password: new password.
285 :raises AuthconnOperationException: if change failed.
288 if is_valid_uuid(user
):
291 user_obj_list
= self
.keystone
.users
.list(name
=user
)
292 if not user_obj_list
:
293 raise AuthconnNotFoundException("User '{}' not found".format(user
))
294 user_id
= user_obj_list
[0].id
296 self
.keystone
.users
.update(user_id
, password
=new_password
, name
=new_name
)
297 except ClientException
as e
:
298 self
.logger
.exception("Error during user password/name update using keystone: {}".format(e
))
299 raise AuthconnOperationException("Error during user password/name update using Keystone: {}".format(e
))
301 def delete_user(self
, user_id
):
305 :param user_id: user identifier.
306 :raises AuthconnOperationException: if user deletion failed.
309 # users = self.keystone.users.list()
310 # user_obj = [user for user in users if user.id == user_id][0]
311 # result, _ = self.keystone.users.delete(user_obj)
313 result
, detail
= self
.keystone
.users
.delete(user_id
)
314 if result
.status_code
!= 204:
315 raise ClientException("error {} {}".format(result
.status_code
, detail
))
318 except ClientException
as e
:
319 self
.logger
.exception("Error during user deletion using keystone: {}".format(e
))
320 raise AuthconnOperationException("Error during user deletion using Keystone: {}".format(e
))
322 def get_user_list(self
, filter_q
=None):
326 :param filter_q: dictionary to filter user list by name (username is also admited) and/or _id
327 :return: returns a list of users.
332 filter_name
= filter_q
.get("name") or filter_q
.get("username")
333 users
= self
.keystone
.users
.list(name
=filter_name
)
335 "username": user
.name
,
338 } for user
in users
if user
.name
!= self
.admin_username
]
340 if filter_q
and filter_q
.get("_id"):
341 users
= [user
for user
in users
if filter_q
["_id"] == user
["_id"]]
344 projects
= self
.keystone
.projects
.list(user
=user
["_id"])
346 "name": project
.name
,
349 } for project
in projects
]
351 for project
in projects
:
352 roles
= self
.keystone
.roles
.list(user
=user
["_id"], project
=project
["_id"])
358 project
["roles"] = roles
360 user
["projects"] = projects
363 except ClientException
as e
:
364 self
.logger
.exception("Error during user listing using keystone: {}".format(e
))
365 raise AuthconnOperationException("Error during user listing using Keystone: {}".format(e
))
367 def get_role_list(self
):
371 :return: returns the list of roles.
374 roles_list
= self
.keystone
.roles
.list()
379 } for role
in roles_list
if role
.name
!= "service"]
382 except ClientException
as e
:
383 self
.logger
.exception("Error during user role listing using keystone: {}".format(e
))
384 raise AuthException("Error during user role listing using Keystone: {}".format(e
),
385 http_code
=HTTPStatus
.UNAUTHORIZED
)
387 def create_role(self
, role
):
391 :param role: role name.
392 :raises AuthconnOperationException: if role creation failed.
395 result
= self
.keystone
.roles
.create(role
)
396 return {"name": result
.name
, "_id": result
.id}
397 except Conflict
as ex
:
398 self
.logger
.info("Duplicate entry: %s", str(ex
))
399 except ClientException
as e
:
400 self
.logger
.exception("Error during role creation using keystone: {}".format(e
))
401 raise AuthconnOperationException("Error during role creation using Keystone: {}".format(e
))
403 def delete_role(self
, role_id
):
407 :param role_id: role identifier.
408 :raises AuthconnOperationException: if role deletion failed.
411 roles
= self
.keystone
.roles
.list()
412 role_obj
= [role
for role
in roles
if role
.id == role_id
][0]
413 result
, detail
= self
.keystone
.roles
.delete(role_obj
)
415 if result
.status_code
!= 204:
416 raise ClientException("error {} {}".format(result
.status_code
, detail
))
419 except ClientException
as e
:
420 self
.logger
.exception("Error during role deletion using keystone: {}".format(e
))
421 raise AuthconnOperationException("Error during role deletion using Keystone: {}".format(e
))
423 def get_project_list(self
, filter_q
=None):
425 Get all the projects.
427 :param filter_q: dictionary to filter project list.
428 :return: list of projects
433 filter_name
= filter_q
.get("name")
434 projects
= self
.keystone
.projects
.list(name
=filter_name
)
437 "name": project
.name
,
439 } for project
in projects
]
441 if filter_q
and filter_q
.get("_id"):
442 projects
= [project
for project
in projects
443 if filter_q
["_id"] == project
["_id"]]
446 except ClientException
as e
:
447 self
.logger
.exception("Error during user project listing using keystone: {}".format(e
))
448 raise AuthException("Error during user project listing using Keystone: {}".format(e
),
449 http_code
=HTTPStatus
.UNAUTHORIZED
)
451 def create_project(self
, project
):
455 :param project: project name.
456 :return: the internal id of the created project
457 :raises AuthconnOperationException: if project creation failed.
460 result
= self
.keystone
.projects
.create(project
, self
.project_domain_name
)
462 except ClientException
as e
:
463 self
.logger
.exception("Error during project creation using keystone: {}".format(e
))
464 raise AuthconnOperationException("Error during project creation using Keystone: {}".format(e
))
466 def delete_project(self
, project_id
):
470 :param project_id: project identifier.
471 :raises AuthconnOperationException: if project deletion failed.
474 # projects = self.keystone.projects.list()
475 # project_obj = [project for project in projects if project.id == project_id][0]
476 # result, _ = self.keystone.projects.delete(project_obj)
478 result
, detail
= self
.keystone
.projects
.delete(project_id
)
479 if result
.status_code
!= 204:
480 raise ClientException("error {} {}".format(result
.status_code
, detail
))
483 except ClientException
as e
:
484 self
.logger
.exception("Error during project deletion using keystone: {}".format(e
))
485 raise AuthconnOperationException("Error during project deletion using Keystone: {}".format(e
))
487 def update_project(self
, project_id
, new_name
):
489 Change the name of a project
490 :param project_id: project to be changed
491 :param new_name: new name
495 self
.keystone
.projects
.update(project_id
, name
=new_name
)
496 except ClientException
as e
:
497 self
.logger
.exception("Error during project update using keystone: {}".format(e
))
498 raise AuthconnOperationException("Error during project deletion using Keystone: {}".format(e
))
500 def assign_role_to_user(self
, user
, project
, role
):
502 Assigning a role to a user in a project.
504 :param user: username.
505 :param project: project name.
506 :param role: role name.
507 :raises AuthconnOperationException: if role assignment failed.
510 if is_valid_uuid(user
):
511 user_obj
= self
.keystone
.users
.get(user
)
513 user_obj_list
= self
.keystone
.users
.list(name
=user
)
514 if not user_obj_list
:
515 raise AuthconnNotFoundException("User '{}' not found".format(user
))
516 user_obj
= user_obj_list
[0]
518 if is_valid_uuid(project
):
519 project_obj
= self
.keystone
.projects
.get(project
)
521 project_obj_list
= self
.keystone
.projects
.list(name
=project
)
522 if not project_obj_list
:
523 raise AuthconnNotFoundException("Project '{}' not found".format(project
))
524 project_obj
= project_obj_list
[0]
526 if is_valid_uuid(role
):
527 role_obj
= self
.keystone
.roles
.get(role
)
529 role_obj_list
= self
.keystone
.roles
.list(name
=role
)
530 if not role_obj_list
:
531 raise AuthconnNotFoundException("Role '{}' not found".format(role
))
532 role_obj
= role_obj_list
[0]
534 self
.keystone
.roles
.grant(role_obj
, user
=user_obj
, project
=project_obj
)
535 except ClientException
as e
:
536 self
.logger
.exception("Error during user role assignment using keystone: {}".format(e
))
537 raise AuthconnOperationException("Error during role '{}' assignment to user '{}' and project '{}' using "
538 "Keystone: {}".format(role
, user
, project
, e
))
540 def remove_role_from_user(self
, user
, project
, role
):
542 Remove a role from a user in a project.
544 :param user: username.
545 :param project: project name or id.
546 :param role: role name or id.
548 :raises AuthconnOperationException: if role assignment revocation failed.
551 if is_valid_uuid(user
):
552 user_obj
= self
.keystone
.users
.get(user
)
554 user_obj_list
= self
.keystone
.users
.list(name
=user
)
555 if not user_obj_list
:
556 raise AuthconnNotFoundException("User '{}' not found".format(user
))
557 user_obj
= user_obj_list
[0]
559 if is_valid_uuid(project
):
560 project_obj
= self
.keystone
.projects
.get(project
)
562 project_obj_list
= self
.keystone
.projects
.list(name
=project
)
563 if not project_obj_list
:
564 raise AuthconnNotFoundException("Project '{}' not found".format(project
))
565 project_obj
= project_obj_list
[0]
567 if is_valid_uuid(role
):
568 role_obj
= self
.keystone
.roles
.get(role
)
570 role_obj_list
= self
.keystone
.roles
.list(name
=role
)
571 if not role_obj_list
:
572 raise AuthconnNotFoundException("Role '{}' not found".format(role
))
573 role_obj
= role_obj_list
[0]
575 self
.keystone
.roles
.revoke(role_obj
, user
=user_obj
, project
=project_obj
)
576 except ClientException
as e
:
577 self
.logger
.exception("Error during user role revocation using keystone: {}".format(e
))
578 raise AuthconnOperationException("Error during role '{}' revocation to user '{}' and project '{}' using "
579 "Keystone: {}".format(role
, user
, project
, e
))