blob: c8d68111949a98e33d6f175b0b7b508dd50999cf [file] [log] [blame]
Eduardo Sousa819d34c2018-07-31 01:20:02 +01001# -*- coding: utf-8 -*-
2
Eduardo Sousad795f872019-02-05 16:05:53 +00003# Copyright 2018 Whitestack, LLC
4#
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
8#
9# http://www.apache.org/licenses/LICENSE-2.0
10#
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
15# under the License.
16#
17# For those usages not covered by the Apache License, Version 2.0 please
18# contact: esousa@whitestack.com or glavado@whitestack.com
19##
20
Eduardo Sousa819d34c2018-07-31 01:20:02 +010021"""
22AuthconnKeystone implements implements the connector for
23Openstack Keystone and leverages the RBAC model, to bring
24it for OSM.
25"""
Eduardo Sousa44603902019-06-04 08:10:32 +010026
Eduardo Sousa819d34c2018-07-31 01:20:02 +010027
28__author__ = "Eduardo Sousa <esousa@whitestack.com>"
29__date__ = "$27-jul-2018 23:59:59$"
30
31from authconn import Authconn, AuthException, AuthconnOperationException
32
33import logging
Eduardo Sousa29933fc2018-11-14 06:36:35 +000034import requests
Eduardo Sousa44603902019-06-04 08:10:32 +010035import time
Eduardo Sousa819d34c2018-07-31 01:20:02 +010036from keystoneauth1 import session
37from keystoneauth1.identity import v3
38from keystoneauth1.exceptions.base import ClientException
Eduardo Sousa29933fc2018-11-14 06:36:35 +000039from keystoneauth1.exceptions.http import Conflict
Eduardo Sousa819d34c2018-07-31 01:20:02 +010040from keystoneclient.v3 import client
41from http import HTTPStatus
Eduardo Sousa44603902019-06-04 08:10:32 +010042from validation import is_valid_uuid
Eduardo Sousa819d34c2018-07-31 01:20:02 +010043
44
45class AuthconnKeystone(Authconn):
46 def __init__(self, config):
47 Authconn.__init__(self, config)
48
49 self.logger = logging.getLogger("nbi.authenticator.keystone")
50
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")
57
Eduardo Sousa29933fc2018-11-14 06:36:35 +000058 # Waiting for Keystone to be up
59 available = None
60 counter = 300
61 while available is None:
62 time.sleep(1)
63 try:
64 result = requests.get(self.auth_url)
65 available = True if result.status_code == 200 else None
66 except Exception:
67 counter -= 1
68 if counter == 0:
69 raise AuthException("Keystone not available after 300s timeout")
70
Eduardo Sousa819d34c2018-07-31 01:20:02 +010071 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)
79
80 def authenticate_with_user_password(self, user, password):
81 """
82 Authenticate a user using username and password.
83
84 :param user: username
85 :param password: password
86 :return: an unscoped token that grants access to project list
87 """
88 try:
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)]
91
92 token = self.keystone.get_raw_token_from_identity_service(
93 auth_url=self.auth_url,
94 username=user,
95 password=password,
96 user_domain_name=self.user_domain_name,
97 project_domain_name=self.project_domain_name)
98
99 return token["auth_token"], project_names
tierno4015b472019-06-10 13:57:29 +0000100 except ClientException as e:
101 self.logger.exception("Error during user authentication using keystone. Method: basic: {}".format(e))
102 raise AuthException("Error during user authentication using Keystone: {}".format(e),
103 http_code=HTTPStatus.UNAUTHORIZED)
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100104
105 def authenticate_with_token(self, token, project=None):
106 """
107 Authenticate a user using a token. Can be used to revalidate the token
108 or to get a scoped token.
109
110 :param token: a valid token.
111 :param project: (optional) project for a scoped token.
112 :return: return a revalidated token, scoped if a project was passed or
113 the previous token was already scoped.
114 """
115 try:
116 token_info = self.keystone.tokens.validate(token=token)
117 projects = self.keystone.projects.list(user=token_info["user"]["id"])
118 project_names = [project.name for project in projects]
119
Eduardo Sousa29933fc2018-11-14 06:36:35 +0000120 new_token = self.keystone.get_raw_token_from_identity_service(
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100121 auth_url=self.auth_url,
122 token=token,
123 project_name=project,
124 user_domain_name=self.user_domain_name,
125 project_domain_name=self.project_domain_name)
126
Eduardo Sousa29933fc2018-11-14 06:36:35 +0000127 return new_token["auth_token"], project_names
tierno4015b472019-06-10 13:57:29 +0000128 except ClientException as e:
129 self.logger.exception("Error during user authentication using keystone. Method: bearer: {}".format(e))
130 raise AuthException("Error during user authentication using Keystone: {}".format(e),
131 http_code=HTTPStatus.UNAUTHORIZED)
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100132
133 def validate_token(self, token):
134 """
135 Check if the token is valid.
136
137 :param token: token to validate
138 :return: dictionary with information associated with the token. If the
139 token is not valid, returns None.
140 """
141 if not token:
142 return
143
144 try:
145 token_info = self.keystone.tokens.validate(token=token)
146
147 return token_info
tierno4015b472019-06-10 13:57:29 +0000148 except ClientException as e:
149 self.logger.exception("Error during token validation using keystone: {}".format(e))
150 raise AuthException("Error during token validation using Keystone: {}".format(e),
151 http_code=HTTPStatus.UNAUTHORIZED)
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100152
153 def revoke_token(self, token):
154 """
155 Invalidate a token.
156
157 :param token: token to be revoked
158 """
159 try:
Eduardo Sousa29933fc2018-11-14 06:36:35 +0000160 self.logger.info("Revoking token: " + token)
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100161 self.keystone.tokens.revoke_token(token=token)
162
163 return True
tierno4015b472019-06-10 13:57:29 +0000164 except ClientException as e:
165 self.logger.exception("Error during token revocation using keystone: {}".format(e))
166 raise AuthException("Error during token revocation using Keystone: {}".format(e),
167 http_code=HTTPStatus.UNAUTHORIZED)
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100168
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100169 def get_user_project_list(self, token):
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100170 """
171 Get all the projects associated with a user.
172
173 :param token: valid token
174 :return: list of projects
175 """
176 try:
177 token_info = self.keystone.tokens.validate(token=token)
178 projects = self.keystone.projects.list(user=token_info["user"]["id"])
179 project_names = [project.name for project in projects]
180
181 return project_names
tierno4015b472019-06-10 13:57:29 +0000182 except ClientException as e:
183 self.logger.exception("Error during user project listing using keystone: {}".format(e))
184 raise AuthException("Error during user project listing using Keystone: {}".format(e),
185 http_code=HTTPStatus.UNAUTHORIZED)
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100186
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100187 def get_user_role_list(self, token):
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100188 """
189 Get role list for a scoped project.
190
191 :param token: scoped token.
192 :return: returns the list of roles for the user in that project. If
193 the token is unscoped it returns None.
194 """
195 try:
196 token_info = self.keystone.tokens.validate(token=token)
Eduardo Sousa29933fc2018-11-14 06:36:35 +0000197 roles_info = self.keystone.roles.list(user=token_info["user"]["id"], project=token_info["project"]["id"])
198
199 roles = [role.name for role in roles_info]
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100200
201 return roles
tierno4015b472019-06-10 13:57:29 +0000202 except ClientException as e:
203 self.logger.exception("Error during user role listing using keystone: {}".format(e))
204 raise AuthException("Error during user role listing using Keystone: {}".format(e),
205 http_code=HTTPStatus.UNAUTHORIZED)
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100206
207 def create_user(self, user, password):
208 """
209 Create a user.
210
211 :param user: username.
212 :param password: password.
213 :raises AuthconnOperationException: if user creation failed.
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100214 :return: returns the id of the user in keystone.
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100215 """
216 try:
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100217 new_user = self.keystone.users.create(user, password=password, domain=self.user_domain_name)
218 return {"username": new_user.name, "_id": new_user.id}
tierno4015b472019-06-10 13:57:29 +0000219 except ClientException as e:
220 self.logger.exception("Error during user creation using keystone: {}".format(e))
221 raise AuthconnOperationException("Error during user creation using Keystone: {}".format(e))
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100222
223 def change_password(self, user, new_password):
224 """
225 Change the user password.
226
227 :param user: username.
228 :param new_password: new password.
229 :raises AuthconnOperationException: if user password change failed.
230 """
231 try:
Eduardo Sousa29933fc2018-11-14 06:36:35 +0000232 user_obj = list(filter(lambda x: x.name == user, self.keystone.users.list()))[0]
233 self.keystone.users.update(user_obj, password=new_password)
tierno4015b472019-06-10 13:57:29 +0000234 except ClientException as e:
235 self.logger.exception("Error during user password update using keystone: {}".format(e))
236 raise AuthconnOperationException("Error during user password update using Keystone: {}".format(e))
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100237
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100238 def delete_user(self, user_id):
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100239 """
240 Delete user.
241
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100242 :param user_id: user identifier.
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100243 :raises AuthconnOperationException: if user deletion failed.
244 """
245 try:
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100246 users = self.keystone.users.list()
247 user_obj = [user for user in users if user.id == user_id][0]
248 result, _ = self.keystone.users.delete(user_obj)
249
250 if result.status_code != 204:
251 raise ClientException("User was not deleted")
252
253 return True
tierno4015b472019-06-10 13:57:29 +0000254 except ClientException as e:
255 self.logger.exception("Error during user deletion using keystone: {}".format(e))
256 raise AuthconnOperationException("Error during user deletion using Keystone: {}".format(e))
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100257
Eduardo Sousa2d5a5152019-05-20 15:41:54 +0100258 def get_user_list(self, filter_q={}):
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100259 """
260 Get user list.
261
Eduardo Sousa2d5a5152019-05-20 15:41:54 +0100262 :param filter_q: dictionary to filter user list.
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100263 :return: returns a list of users.
264 """
265 try:
266 users = self.keystone.users.list()
267 users = [{
268 "username": user.name,
Eduardo Sousa203bad82019-05-23 01:41:18 +0100269 "_id": user.id,
270 "id": user.id
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100271 } for user in users if user.name != self.admin_username]
272
Eduardo Sousa203bad82019-05-23 01:41:18 +0100273 allowed_fields = ["_id", "id", "username"]
Eduardo Sousa2d5a5152019-05-20 15:41:54 +0100274 for key in filter_q.keys():
275 if key not in allowed_fields:
276 continue
277
278 users = [user for user in users
279 if filter_q[key] == user[key]]
280
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100281 for user in users:
282 projects = self.keystone.projects.list(user=user["_id"])
283 projects = [{
284 "name": project.name,
Eduardo Sousa203bad82019-05-23 01:41:18 +0100285 "_id": project.id,
286 "id": project.id
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100287 } for project in projects]
288
289 for project in projects:
290 roles = self.keystone.roles.list(user=user["_id"], project=project["_id"])
291 roles = [{
292 "name": role.name,
Eduardo Sousa203bad82019-05-23 01:41:18 +0100293 "_id": role.id,
294 "id": role.id
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100295 } for role in roles]
296 project["roles"] = roles
297
298 user["projects"] = projects
299
300 return users
tierno4015b472019-06-10 13:57:29 +0000301 except ClientException as e:
302 self.logger.exception("Error during user listing using keystone: {}".format(e))
303 raise AuthconnOperationException("Error during user listing using Keystone: {}".format(e))
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100304
305 def get_role_list(self):
306 """
307 Get role list.
308
Eduardo Sousa37de0912019-05-23 02:17:22 +0100309 :return: returns the list of roles.
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100310 """
311 try:
312 roles_list = self.keystone.roles.list()
313
314 roles = [{
315 "name": role.name,
316 "_id": role.id
317 } for role in roles_list if role.name != "service"]
318
319 return roles
tierno4015b472019-06-10 13:57:29 +0000320 except ClientException as e:
321 self.logger.exception("Error during user role listing using keystone: {}".format(e))
322 raise AuthException("Error during user role listing using Keystone: {}".format(e),
323 http_code=HTTPStatus.UNAUTHORIZED)
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100324
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100325 def create_role(self, role):
326 """
327 Create a role.
328
329 :param role: role name.
330 :raises AuthconnOperationException: if role creation failed.
331 """
332 try:
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100333 result = self.keystone.roles.create(role)
334 return {"name": result.name, "_id": result.id}
Eduardo Sousa29933fc2018-11-14 06:36:35 +0000335 except Conflict as ex:
336 self.logger.info("Duplicate entry: %s", str(ex))
tierno4015b472019-06-10 13:57:29 +0000337 except ClientException as e:
338 self.logger.exception("Error during role creation using keystone: {}".format(e))
339 raise AuthconnOperationException("Error during role creation using Keystone: {}".format(e))
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100340
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100341 def delete_role(self, role_id):
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100342 """
343 Delete a role.
344
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100345 :param role_id: role identifier.
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100346 :raises AuthconnOperationException: if role deletion failed.
347 """
348 try:
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100349 roles = self.keystone.roles.list()
350 role_obj = [role for role in roles if role.id == role_id][0]
351 result, _ = self.keystone.roles.delete(role_obj)
352
353 if result.status_code != 204:
354 raise ClientException("Role was not deleted")
355
356 return True
tierno4015b472019-06-10 13:57:29 +0000357 except ClientException as e:
358 self.logger.exception("Error during role deletion using keystone: {}".format(e))
359 raise AuthconnOperationException("Error during role deletion using Keystone: {}".format(e))
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100360
Eduardo Sousafa54cd92019-05-20 15:58:41 +0100361 def get_project_list(self, filter_q={}):
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100362 """
363 Get all the projects.
364
Eduardo Sousafa54cd92019-05-20 15:58:41 +0100365 :param filter_q: dictionary to filter project list.
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100366 :return: list of projects
367 """
368 try:
369 projects = self.keystone.projects.list()
370 projects = [{
371 "name": project.name,
372 "_id": project.id
373 } for project in projects if project.name != self.admin_project]
374
Eduardo Sousafa54cd92019-05-20 15:58:41 +0100375 allowed_fields = ["_id", "name"]
376 for key in filter_q.keys():
377 if key not in allowed_fields:
378 continue
379
380 projects = [project for project in projects
381 if filter_q[key] == project[key]]
382
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100383 return projects
tierno4015b472019-06-10 13:57:29 +0000384 except ClientException as e:
385 self.logger.exception("Error during user project listing using keystone: {}".format(e))
386 raise AuthException("Error during user project listing using Keystone: {}".format(e),
387 http_code=HTTPStatus.UNAUTHORIZED)
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100388
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100389 def create_project(self, project):
390 """
391 Create a project.
392
393 :param project: project name.
tierno4015b472019-06-10 13:57:29 +0000394 :return: the internal id of the created project
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100395 :raises AuthconnOperationException: if project creation failed.
396 """
397 try:
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100398 result = self.keystone.projects.create(project, self.project_domain_name)
tierno4015b472019-06-10 13:57:29 +0000399 return result.id
400 except ClientException as e:
401 self.logger.exception("Error during project creation using keystone: {}".format(e))
402 raise AuthconnOperationException("Error during project creation using Keystone: {}".format(e))
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100403
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100404 def delete_project(self, project_id):
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100405 """
406 Delete a project.
407
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100408 :param project_id: project identifier.
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100409 :raises AuthconnOperationException: if project deletion failed.
410 """
411 try:
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100412 projects = self.keystone.projects.list()
413 project_obj = [project for project in projects if project.id == project_id][0]
414 result, _ = self.keystone.projects.delete(project_obj)
415
416 if result.status_code != 204:
417 raise ClientException("Project was not deleted")
418
419 return True
tierno4015b472019-06-10 13:57:29 +0000420 except ClientException as e:
421 self.logger.exception("Error during project deletion using keystone: {}".format(e))
422 raise AuthconnOperationException("Error during project deletion using Keystone: {}".format(e))
423
424 def update_project(self, project_id, new_name):
425 """
426 Change the name of a project
427 :param project_id: project to be changed
428 :param new_name: new name
429 :return: None
430 """
431 try:
432 self.keystone.projects.update(project_id, name=new_name)
433 except ClientException as e:
434 self.logger.exception("Error during project update using keystone: {}".format(e))
435 raise AuthconnOperationException("Error during project deletion using Keystone: {}".format(e))
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100436
437 def assign_role_to_user(self, user, project, role):
438 """
439 Assigning a role to a user in a project.
440
441 :param user: username.
442 :param project: project name.
443 :param role: role name.
444 :raises AuthconnOperationException: if role assignment failed.
445 """
446 try:
Eduardo Sousa44603902019-06-04 08:10:32 +0100447 if is_valid_uuid(user):
448 user_obj = self.keystone.users.get(user)
449 else:
450 user_obj = self.keystone.users.list(name=user)[0]
451
452 if is_valid_uuid(project):
453 project_obj = self.keystone.projects.get(project)
454 else:
455 project_obj = self.keystone.projects.list(name=project)[0]
456
457 if is_valid_uuid(role):
458 role_obj = self.keystone.roles.get(role)
459 else:
460 role_obj = self.keystone.roles.list(name=role)[0]
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100461
Eduardo Sousa29933fc2018-11-14 06:36:35 +0000462 self.keystone.roles.grant(role_obj, user=user_obj, project=project_obj)
tierno4015b472019-06-10 13:57:29 +0000463 except ClientException as e:
464 self.logger.exception("Error during user role assignment using keystone: {}".format(e))
465 raise AuthconnOperationException("Error during user role assignment using Keystone: {}".format(e))
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100466
467 def remove_role_from_user(self, user, project, role):
468 """
469 Remove a role from a user in a project.
470
471 :param user: username.
472 :param project: project name.
473 :param role: role name.
474 :raises AuthconnOperationException: if role assignment revocation failed.
475 """
476 try:
Eduardo Sousa29933fc2018-11-14 06:36:35 +0000477 user_obj = list(filter(lambda x: x.name == user, self.keystone.users.list()))[0]
478 project_obj = list(filter(lambda x: x.name == project, self.keystone.projects.list()))[0]
479 role_obj = list(filter(lambda x: x.name == role, self.keystone.roles.list()))[0]
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100480
Eduardo Sousa29933fc2018-11-14 06:36:35 +0000481 self.keystone.roles.revoke(role_obj, user=user_obj, project=project_obj)
tierno4015b472019-06-10 13:57:29 +0000482 except ClientException as e:
483 self.logger.exception("Error during user role revocation using keystone: {}".format(e))
484 raise AuthconnOperationException("Error during user role revocation using Keystone: {}".format(e))