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
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 ClientException
as e
:
272 self
.logger
.exception("Error during user creation using keystone: {}".format(e
))
273 raise AuthconnOperationException("Error during user creation using Keystone: {}".format(e
))
275 def change_password(self
, user
, new_password
):
277 Change the user password.
279 :param user: username.
280 :param new_password: new password.
281 :raises AuthconnOperationException: if user password change failed.
284 user_obj
= list(filter(lambda x
: x
.name
== user
, self
.keystone
.users
.list()))[0]
285 self
.keystone
.users
.update(user_obj
, password
=new_password
)
286 except ClientException
as e
:
287 self
.logger
.exception("Error during user password update using keystone: {}".format(e
))
288 raise AuthconnOperationException("Error during user password update using Keystone: {}".format(e
))
290 def delete_user(self
, user_id
):
294 :param user_id: user identifier.
295 :raises AuthconnOperationException: if user deletion failed.
298 # users = self.keystone.users.list()
299 # user_obj = [user for user in users if user.id == user_id][0]
300 # result, _ = self.keystone.users.delete(user_obj)
302 result
, detail
= self
.keystone
.users
.delete(user_id
)
303 if result
.status_code
!= 204:
304 raise ClientException("error {} {}".format(result
.status_code
, detail
))
307 except ClientException
as e
:
308 self
.logger
.exception("Error during user deletion using keystone: {}".format(e
))
309 raise AuthconnOperationException("Error during user deletion using Keystone: {}".format(e
))
311 def get_user_list(self
, filter_q
={}):
315 :param filter_q: dictionary to filter user list.
316 :return: returns a list of users.
319 users
= self
.keystone
.users
.list()
321 "username": user
.name
,
324 } for user
in users
if user
.name
!= self
.admin_username
]
326 allowed_fields
= ["_id", "id", "username"]
327 for key
in filter_q
.keys():
328 if key
not in allowed_fields
:
331 users
= [user
for user
in users
332 if filter_q
[key
] == user
[key
]]
335 projects
= self
.keystone
.projects
.list(user
=user
["_id"])
337 "name": project
.name
,
340 } for project
in projects
]
342 for project
in projects
:
343 roles
= self
.keystone
.roles
.list(user
=user
["_id"], project
=project
["_id"])
349 project
["roles"] = roles
351 user
["projects"] = projects
354 except ClientException
as e
:
355 self
.logger
.exception("Error during user listing using keystone: {}".format(e
))
356 raise AuthconnOperationException("Error during user listing using Keystone: {}".format(e
))
358 def get_role_list(self
):
362 :return: returns the list of roles.
365 roles_list
= self
.keystone
.roles
.list()
370 } for role
in roles_list
if role
.name
!= "service"]
373 except ClientException
as e
:
374 self
.logger
.exception("Error during user role listing using keystone: {}".format(e
))
375 raise AuthException("Error during user role listing using Keystone: {}".format(e
),
376 http_code
=HTTPStatus
.UNAUTHORIZED
)
378 def create_role(self
, role
):
382 :param role: role name.
383 :raises AuthconnOperationException: if role creation failed.
386 result
= self
.keystone
.roles
.create(role
)
387 return {"name": result
.name
, "_id": result
.id}
388 except Conflict
as ex
:
389 self
.logger
.info("Duplicate entry: %s", str(ex
))
390 except ClientException
as e
:
391 self
.logger
.exception("Error during role creation using keystone: {}".format(e
))
392 raise AuthconnOperationException("Error during role creation using Keystone: {}".format(e
))
394 def delete_role(self
, role_id
):
398 :param role_id: role identifier.
399 :raises AuthconnOperationException: if role deletion failed.
402 roles
= self
.keystone
.roles
.list()
403 role_obj
= [role
for role
in roles
if role
.id == role_id
][0]
404 result
, detail
= self
.keystone
.roles
.delete(role_obj
)
406 if result
.status_code
!= 204:
407 raise ClientException("error {} {}".format(result
.status_code
, detail
))
410 except ClientException
as e
:
411 self
.logger
.exception("Error during role deletion using keystone: {}".format(e
))
412 raise AuthconnOperationException("Error during role deletion using Keystone: {}".format(e
))
414 def get_project_list(self
, filter_q
=None):
416 Get all the projects.
418 :param filter_q: dictionary to filter project list.
419 :return: list of projects
424 filter_name
= filter_q
.get("name")
425 projects
= self
.keystone
.projects
.list(name
=filter_name
)
428 "name": project
.name
,
430 } for project
in projects
]
432 if filter_q
and filter_q
.get("_id"):
433 projects
= [project
for project
in projects
434 if filter_q
["_id"] == project
["_id"]]
437 except ClientException
as e
:
438 self
.logger
.exception("Error during user project listing using keystone: {}".format(e
))
439 raise AuthException("Error during user project listing using Keystone: {}".format(e
),
440 http_code
=HTTPStatus
.UNAUTHORIZED
)
442 def create_project(self
, project
):
446 :param project: project name.
447 :return: the internal id of the created project
448 :raises AuthconnOperationException: if project creation failed.
451 result
= self
.keystone
.projects
.create(project
, self
.project_domain_name
)
453 except ClientException
as e
:
454 self
.logger
.exception("Error during project creation using keystone: {}".format(e
))
455 raise AuthconnOperationException("Error during project creation using Keystone: {}".format(e
))
457 def delete_project(self
, project_id
):
461 :param project_id: project identifier.
462 :raises AuthconnOperationException: if project deletion failed.
465 # projects = self.keystone.projects.list()
466 # project_obj = [project for project in projects if project.id == project_id][0]
467 # result, _ = self.keystone.projects.delete(project_obj)
469 result
, detail
= self
.keystone
.projects
.delete(project_id
)
470 if result
.status_code
!= 204:
471 raise ClientException("error {} {}".format(result
.status_code
, detail
))
474 except ClientException
as e
:
475 self
.logger
.exception("Error during project deletion using keystone: {}".format(e
))
476 raise AuthconnOperationException("Error during project deletion using Keystone: {}".format(e
))
478 def update_project(self
, project_id
, new_name
):
480 Change the name of a project
481 :param project_id: project to be changed
482 :param new_name: new name
486 self
.keystone
.projects
.update(project_id
, name
=new_name
)
487 except ClientException
as e
:
488 self
.logger
.exception("Error during project update using keystone: {}".format(e
))
489 raise AuthconnOperationException("Error during project deletion using Keystone: {}".format(e
))
491 def assign_role_to_user(self
, user
, project
, role
):
493 Assigning a role to a user in a project.
495 :param user: username.
496 :param project: project name.
497 :param role: role name.
498 :raises AuthconnOperationException: if role assignment failed.
501 if is_valid_uuid(user
):
502 user_obj
= self
.keystone
.users
.get(user
)
504 user_obj
= self
.keystone
.users
.list(name
=user
)[0]
506 if is_valid_uuid(project
):
507 project_obj
= self
.keystone
.projects
.get(project
)
509 project_obj
= self
.keystone
.projects
.list(name
=project
)[0]
511 if is_valid_uuid(role
):
512 role_obj
= self
.keystone
.roles
.get(role
)
514 role_obj
= self
.keystone
.roles
.list(name
=role
)[0]
516 self
.keystone
.roles
.grant(role_obj
, user
=user_obj
, project
=project_obj
)
517 except ClientException
as e
:
518 self
.logger
.exception("Error during user role assignment using keystone: {}".format(e
))
519 raise AuthconnOperationException("Error during user role assignment using Keystone: {}".format(e
))
521 def remove_role_from_user(self
, user
, project
, role
):
523 Remove a role from a user in a project.
525 :param user: username.
526 :param project: project name.
527 :param role: role name.
528 :raises AuthconnOperationException: if role assignment revocation failed.
531 user_obj
= list(filter(lambda x
: x
.name
== user
, self
.keystone
.users
.list()))[0]
532 project_obj
= list(filter(lambda x
: x
.name
== project
, self
.keystone
.projects
.list()))[0]
533 role_obj
= list(filter(lambda x
: x
.name
== role
, self
.keystone
.roles
.list()))[0]
535 self
.keystone
.roles
.revoke(role_obj
, user
=user_obj
, project
=project_obj
)
536 except ClientException
as e
:
537 self
.logger
.exception("Error during user role revocation using keystone: {}".format(e
))
538 raise AuthconnOperationException("Error during user role revocation using Keystone: {}".format(e
))