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_with_user_password(self
, user
, password
):
82 Authenticate a user using username and password.
85 :param password: password
86 :return: an unscoped token that grants access to project list
89 user_id
= list(filter(lambda x
: x
.name
== user
, self
.keystone
.users
.list()))[0].id
90 project_names
= [project
.name
for project
in self
.keystone
.projects
.list(user
=user_id
)]
92 token
= self
.keystone
.get_raw_token_from_identity_service(
93 auth_url
=self
.auth_url
,
96 user_domain_name
=self
.user_domain_name
,
97 project_domain_name
=self
.project_domain_name
)
99 return token
["auth_token"], project_names
100 except ClientException
:
101 self
.logger
.exception("Error during user authentication using keystone. Method: basic")
102 raise AuthException("Error during user authentication using Keystone", http_code
=HTTPStatus
.UNAUTHORIZED
)
104 def authenticate_with_token(self
, token
, project
=None):
106 Authenticate a user using a token. Can be used to revalidate the token
107 or to get a scoped token.
109 :param token: a valid token.
110 :param project: (optional) project for a scoped token.
111 :return: return a revalidated token, scoped if a project was passed or
112 the previous token was already scoped.
115 token_info
= self
.keystone
.tokens
.validate(token
=token
)
116 projects
= self
.keystone
.projects
.list(user
=token_info
["user"]["id"])
117 project_names
= [project
.name
for project
in projects
]
119 new_token
= self
.keystone
.get_raw_token_from_identity_service(
120 auth_url
=self
.auth_url
,
122 project_name
=project
,
123 user_domain_name
=self
.user_domain_name
,
124 project_domain_name
=self
.project_domain_name
)
126 return new_token
["auth_token"], project_names
127 except ClientException
:
128 self
.logger
.exception("Error during user authentication using keystone. Method: bearer")
129 raise AuthException("Error during user authentication using Keystone", http_code
=HTTPStatus
.UNAUTHORIZED
)
131 def validate_token(self
, token
):
133 Check if the token is valid.
135 :param token: token to validate
136 :return: dictionary with information associated with the token. If the
137 token is not valid, returns None.
143 token_info
= self
.keystone
.tokens
.validate(token
=token
)
146 except ClientException
:
147 self
.logger
.exception("Error during token validation using keystone")
148 raise AuthException("Error during token validation using Keystone", http_code
=HTTPStatus
.UNAUTHORIZED
)
150 def revoke_token(self
, token
):
154 :param token: token to be revoked
157 self
.logger
.info("Revoking token: " + token
)
158 self
.keystone
.tokens
.revoke_token(token
=token
)
161 except ClientException
:
162 self
.logger
.exception("Error during token revocation using keystone")
163 raise AuthException("Error during token revocation using Keystone", http_code
=HTTPStatus
.UNAUTHORIZED
)
165 def get_user_project_list(self
, token
):
167 Get all the projects associated with a user.
169 :param token: valid token
170 :return: list of projects
173 token_info
= self
.keystone
.tokens
.validate(token
=token
)
174 projects
= self
.keystone
.projects
.list(user
=token_info
["user"]["id"])
175 project_names
= [project
.name
for project
in projects
]
178 except ClientException
:
179 self
.logger
.exception("Error during user project listing using keystone")
180 raise AuthException("Error during user project listing using Keystone", http_code
=HTTPStatus
.UNAUTHORIZED
)
182 def get_user_role_list(self
, token
):
184 Get role list for a scoped project.
186 :param token: scoped token.
187 :return: returns the list of roles for the user in that project. If
188 the token is unscoped it returns None.
191 token_info
= self
.keystone
.tokens
.validate(token
=token
)
192 roles_info
= self
.keystone
.roles
.list(user
=token_info
["user"]["id"], project
=token_info
["project"]["id"])
194 roles
= [role
.name
for role
in roles_info
]
197 except ClientException
:
198 self
.logger
.exception("Error during user role listing using keystone")
199 raise AuthException("Error during user role listing using Keystone", http_code
=HTTPStatus
.UNAUTHORIZED
)
201 def create_user(self
, user
, password
):
205 :param user: username.
206 :param password: password.
207 :raises AuthconnOperationException: if user creation failed.
208 :return: returns the id of the user in keystone.
211 new_user
= self
.keystone
.users
.create(user
, password
=password
, domain
=self
.user_domain_name
)
212 return {"username": new_user
.name
, "_id": new_user
.id}
213 except ClientException
:
214 self
.logger
.exception("Error during user creation using keystone")
215 raise AuthconnOperationException("Error during user creation using Keystone")
217 def change_password(self
, user
, new_password
):
219 Change the user password.
221 :param user: username.
222 :param new_password: new password.
223 :raises AuthconnOperationException: if user password change failed.
226 user_obj
= list(filter(lambda x
: x
.name
== user
, self
.keystone
.users
.list()))[0]
227 self
.keystone
.users
.update(user_obj
, password
=new_password
)
228 except ClientException
:
229 self
.logger
.exception("Error during user password update using keystone")
230 raise AuthconnOperationException("Error during user password update using Keystone")
232 def delete_user(self
, user_id
):
236 :param user_id: user identifier.
237 :raises AuthconnOperationException: if user deletion failed.
240 users
= self
.keystone
.users
.list()
241 user_obj
= [user
for user
in users
if user
.id == user_id
][0]
242 result
, _
= self
.keystone
.users
.delete(user_obj
)
244 if result
.status_code
!= 204:
245 raise ClientException("User was not deleted")
248 except ClientException
:
249 self
.logger
.exception("Error during user deletion using keystone")
250 raise AuthconnOperationException("Error during user deletion using Keystone")
252 def get_user_list(self
, filter_q
={}):
256 :param filter_q: dictionary to filter user list.
257 :return: returns a list of users.
260 users
= self
.keystone
.users
.list()
262 "username": user
.name
,
265 } for user
in users
if user
.name
!= self
.admin_username
]
267 allowed_fields
= ["_id", "id", "username"]
268 for key
in filter_q
.keys():
269 if key
not in allowed_fields
:
272 users
= [user
for user
in users
273 if filter_q
[key
] == user
[key
]]
276 projects
= self
.keystone
.projects
.list(user
=user
["_id"])
278 "name": project
.name
,
281 } for project
in projects
]
283 for project
in projects
:
284 roles
= self
.keystone
.roles
.list(user
=user
["_id"], project
=project
["_id"])
290 project
["roles"] = roles
292 user
["projects"] = projects
295 except ClientException
:
296 self
.logger
.exception("Error during user listing using keystone")
297 raise AuthconnOperationException("Error during user listing using Keystone")
299 def get_role_list(self
):
303 :return: returns the list of roles.
306 roles_list
= self
.keystone
.roles
.list()
311 } for role
in roles_list
if role
.name
!= "service"]
314 except ClientException
:
315 self
.logger
.exception("Error during user role listing using keystone")
316 raise AuthException("Error during user role listing using Keystone", http_code
=HTTPStatus
.UNAUTHORIZED
)
318 def create_role(self
, role
):
322 :param role: role name.
323 :raises AuthconnOperationException: if role creation failed.
326 result
= self
.keystone
.roles
.create(role
)
327 return {"name": result
.name
, "_id": result
.id}
328 except Conflict
as ex
:
329 self
.logger
.info("Duplicate entry: %s", str(ex
))
330 except ClientException
:
331 self
.logger
.exception("Error during role creation using keystone")
332 raise AuthconnOperationException("Error during role creation using Keystone")
334 def delete_role(self
, role_id
):
338 :param role_id: role identifier.
339 :raises AuthconnOperationException: if role deletion failed.
342 roles
= self
.keystone
.roles
.list()
343 role_obj
= [role
for role
in roles
if role
.id == role_id
][0]
344 result
, _
= self
.keystone
.roles
.delete(role_obj
)
346 if result
.status_code
!= 204:
347 raise ClientException("Role was not deleted")
350 except ClientException
:
351 self
.logger
.exception("Error during role deletion using keystone")
352 raise AuthconnOperationException("Error during role deletion using Keystone")
354 def get_project_list(self
, filter_q
={}):
356 Get all the projects.
358 :param filter_q: dictionary to filter project list.
359 :return: list of projects
362 projects
= self
.keystone
.projects
.list()
364 "name": project
.name
,
366 } for project
in projects
if project
.name
!= self
.admin_project
]
368 allowed_fields
= ["_id", "name"]
369 for key
in filter_q
.keys():
370 if key
not in allowed_fields
:
373 projects
= [project
for project
in projects
374 if filter_q
[key
] == project
[key
]]
377 except ClientException
:
378 self
.logger
.exception("Error during user project listing using keystone")
379 raise AuthException("Error during user project listing using Keystone", http_code
=HTTPStatus
.UNAUTHORIZED
)
381 def create_project(self
, project
):
385 :param project: project name.
386 :raises AuthconnOperationException: if project creation failed.
389 result
= self
.keystone
.projects
.create(project
, self
.project_domain_name
)
390 return {"name": result
.name
, "_id": result
.id}
391 except ClientException
:
392 self
.logger
.exception("Error during project creation using keystone")
393 raise AuthconnOperationException("Error during project creation using Keystone")
395 def delete_project(self
, project_id
):
399 :param project_id: project identifier.
400 :raises AuthconnOperationException: if project deletion failed.
403 projects
= self
.keystone
.projects
.list()
404 project_obj
= [project
for project
in projects
if project
.id == project_id
][0]
405 result
, _
= self
.keystone
.projects
.delete(project_obj
)
407 if result
.status_code
!= 204:
408 raise ClientException("Project was not deleted")
411 except ClientException
:
412 self
.logger
.exception("Error during project deletion using keystone")
413 raise AuthconnOperationException("Error during project deletion using Keystone")
415 def assign_role_to_user(self
, user
, project
, role
):
417 Assigning a role to a user in a project.
419 :param user: username.
420 :param project: project name.
421 :param role: role name.
422 :raises AuthconnOperationException: if role assignment failed.
425 if is_valid_uuid(user
):
426 user_obj
= self
.keystone
.users
.get(user
)
428 user_obj
= self
.keystone
.users
.list(name
=user
)[0]
430 if is_valid_uuid(project
):
431 project_obj
= self
.keystone
.projects
.get(project
)
433 project_obj
= self
.keystone
.projects
.list(name
=project
)[0]
435 if is_valid_uuid(role
):
436 role_obj
= self
.keystone
.roles
.get(role
)
438 role_obj
= self
.keystone
.roles
.list(name
=role
)[0]
440 self
.keystone
.roles
.grant(role_obj
, user
=user_obj
, project
=project_obj
)
441 except ClientException
:
442 self
.logger
.exception("Error during user role assignment using keystone")
443 raise AuthconnOperationException("Error during user role assignment using Keystone")
445 def remove_role_from_user(self
, user
, project
, role
):
447 Remove a role from a user in a project.
449 :param user: username.
450 :param project: project name.
451 :param role: role name.
452 :raises AuthconnOperationException: if role assignment revocation failed.
455 user_obj
= list(filter(lambda x
: x
.name
== user
, self
.keystone
.users
.list()))[0]
456 project_obj
= list(filter(lambda x
: x
.name
== project
, self
.keystone
.projects
.list()))[0]
457 role_obj
= list(filter(lambda x
: x
.name
== role
, self
.keystone
.roles
.list()))[0]
459 self
.keystone
.roles
.revoke(role_obj
, user
=user_obj
, project
=project_obj
)
460 except ClientException
:
461 self
.logger
.exception("Error during user role revocation using keystone")
462 raise AuthconnOperationException("Error during user role revocation using Keystone")