blob: f819d3fb84664a3889568c4cde01bb64cefeec34 [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
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)
103
104 def authenticate_with_token(self, token, project=None):
105 """
106 Authenticate a user using a token. Can be used to revalidate the token
107 or to get a scoped token.
108
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.
113 """
114 try:
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]
118
Eduardo Sousa29933fc2018-11-14 06:36:35 +0000119 new_token = self.keystone.get_raw_token_from_identity_service(
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100120 auth_url=self.auth_url,
121 token=token,
122 project_name=project,
123 user_domain_name=self.user_domain_name,
124 project_domain_name=self.project_domain_name)
125
Eduardo Sousa29933fc2018-11-14 06:36:35 +0000126 return new_token["auth_token"], project_names
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100127 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)
130
131 def validate_token(self, token):
132 """
133 Check if the token is valid.
134
135 :param token: token to validate
136 :return: dictionary with information associated with the token. If the
137 token is not valid, returns None.
138 """
139 if not token:
140 return
141
142 try:
143 token_info = self.keystone.tokens.validate(token=token)
144
145 return token_info
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)
149
150 def revoke_token(self, token):
151 """
152 Invalidate a token.
153
154 :param token: token to be revoked
155 """
156 try:
Eduardo Sousa29933fc2018-11-14 06:36:35 +0000157 self.logger.info("Revoking token: " + token)
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100158 self.keystone.tokens.revoke_token(token=token)
159
160 return True
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)
164
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100165 def get_user_project_list(self, token):
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100166 """
167 Get all the projects associated with a user.
168
169 :param token: valid token
170 :return: list of projects
171 """
172 try:
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]
176
177 return project_names
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)
181
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100182 def get_user_role_list(self, token):
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100183 """
184 Get role list for a scoped project.
185
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.
189 """
190 try:
191 token_info = self.keystone.tokens.validate(token=token)
Eduardo Sousa29933fc2018-11-14 06:36:35 +0000192 roles_info = self.keystone.roles.list(user=token_info["user"]["id"], project=token_info["project"]["id"])
193
194 roles = [role.name for role in roles_info]
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100195
196 return roles
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)
200
201 def create_user(self, user, password):
202 """
203 Create a user.
204
205 :param user: username.
206 :param password: password.
207 :raises AuthconnOperationException: if user creation failed.
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100208 :return: returns the id of the user in keystone.
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100209 """
210 try:
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100211 new_user = self.keystone.users.create(user, password=password, domain=self.user_domain_name)
212 return {"username": new_user.name, "_id": new_user.id}
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100213 except ClientException:
214 self.logger.exception("Error during user creation using keystone")
215 raise AuthconnOperationException("Error during user creation using Keystone")
216
217 def change_password(self, user, new_password):
218 """
219 Change the user password.
220
221 :param user: username.
222 :param new_password: new password.
223 :raises AuthconnOperationException: if user password change failed.
224 """
225 try:
Eduardo Sousa29933fc2018-11-14 06:36:35 +0000226 user_obj = list(filter(lambda x: x.name == user, self.keystone.users.list()))[0]
227 self.keystone.users.update(user_obj, password=new_password)
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100228 except ClientException:
229 self.logger.exception("Error during user password update using keystone")
230 raise AuthconnOperationException("Error during user password update using Keystone")
231
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100232 def delete_user(self, user_id):
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100233 """
234 Delete user.
235
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100236 :param user_id: user identifier.
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100237 :raises AuthconnOperationException: if user deletion failed.
238 """
239 try:
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100240 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)
243
244 if result.status_code != 204:
245 raise ClientException("User was not deleted")
246
247 return True
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100248 except ClientException:
249 self.logger.exception("Error during user deletion using keystone")
250 raise AuthconnOperationException("Error during user deletion using Keystone")
251
Eduardo Sousa2d5a5152019-05-20 15:41:54 +0100252 def get_user_list(self, filter_q={}):
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100253 """
254 Get user list.
255
Eduardo Sousa2d5a5152019-05-20 15:41:54 +0100256 :param filter_q: dictionary to filter user list.
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100257 :return: returns a list of users.
258 """
259 try:
260 users = self.keystone.users.list()
261 users = [{
262 "username": user.name,
Eduardo Sousa203bad82019-05-23 01:41:18 +0100263 "_id": user.id,
264 "id": user.id
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100265 } for user in users if user.name != self.admin_username]
266
Eduardo Sousa203bad82019-05-23 01:41:18 +0100267 allowed_fields = ["_id", "id", "username"]
Eduardo Sousa2d5a5152019-05-20 15:41:54 +0100268 for key in filter_q.keys():
269 if key not in allowed_fields:
270 continue
271
272 users = [user for user in users
273 if filter_q[key] == user[key]]
274
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100275 for user in users:
276 projects = self.keystone.projects.list(user=user["_id"])
277 projects = [{
278 "name": project.name,
Eduardo Sousa203bad82019-05-23 01:41:18 +0100279 "_id": project.id,
280 "id": project.id
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100281 } for project in projects]
282
283 for project in projects:
284 roles = self.keystone.roles.list(user=user["_id"], project=project["_id"])
285 roles = [{
286 "name": role.name,
Eduardo Sousa203bad82019-05-23 01:41:18 +0100287 "_id": role.id,
288 "id": role.id
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100289 } for role in roles]
290 project["roles"] = roles
291
292 user["projects"] = projects
293
294 return users
295 except ClientException:
296 self.logger.exception("Error during user listing using keystone")
297 raise AuthconnOperationException("Error during user listing using Keystone")
298
299 def get_role_list(self):
300 """
301 Get role list.
302
Eduardo Sousa37de0912019-05-23 02:17:22 +0100303 :return: returns the list of roles.
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100304 """
305 try:
306 roles_list = self.keystone.roles.list()
307
308 roles = [{
309 "name": role.name,
310 "_id": role.id
311 } for role in roles_list if role.name != "service"]
312
313 return roles
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)
317
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100318 def create_role(self, role):
319 """
320 Create a role.
321
322 :param role: role name.
323 :raises AuthconnOperationException: if role creation failed.
324 """
325 try:
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100326 result = self.keystone.roles.create(role)
327 return {"name": result.name, "_id": result.id}
Eduardo Sousa29933fc2018-11-14 06:36:35 +0000328 except Conflict as ex:
329 self.logger.info("Duplicate entry: %s", str(ex))
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100330 except ClientException:
331 self.logger.exception("Error during role creation using keystone")
332 raise AuthconnOperationException("Error during role creation using Keystone")
333
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100334 def delete_role(self, role_id):
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100335 """
336 Delete a role.
337
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100338 :param role_id: role identifier.
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100339 :raises AuthconnOperationException: if role deletion failed.
340 """
341 try:
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100342 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)
345
346 if result.status_code != 204:
347 raise ClientException("Role was not deleted")
348
349 return True
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100350 except ClientException:
351 self.logger.exception("Error during role deletion using keystone")
352 raise AuthconnOperationException("Error during role deletion using Keystone")
353
Eduardo Sousafa54cd92019-05-20 15:58:41 +0100354 def get_project_list(self, filter_q={}):
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100355 """
356 Get all the projects.
357
Eduardo Sousafa54cd92019-05-20 15:58:41 +0100358 :param filter_q: dictionary to filter project list.
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100359 :return: list of projects
360 """
361 try:
362 projects = self.keystone.projects.list()
363 projects = [{
364 "name": project.name,
365 "_id": project.id
366 } for project in projects if project.name != self.admin_project]
367
Eduardo Sousafa54cd92019-05-20 15:58:41 +0100368 allowed_fields = ["_id", "name"]
369 for key in filter_q.keys():
370 if key not in allowed_fields:
371 continue
372
373 projects = [project for project in projects
374 if filter_q[key] == project[key]]
375
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100376 return projects
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)
380
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100381 def create_project(self, project):
382 """
383 Create a project.
384
385 :param project: project name.
386 :raises AuthconnOperationException: if project creation failed.
387 """
388 try:
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100389 result = self.keystone.projects.create(project, self.project_domain_name)
390 return {"name": result.name, "_id": result.id}
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100391 except ClientException:
392 self.logger.exception("Error during project creation using keystone")
393 raise AuthconnOperationException("Error during project creation using Keystone")
394
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100395 def delete_project(self, project_id):
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100396 """
397 Delete a project.
398
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100399 :param project_id: project identifier.
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100400 :raises AuthconnOperationException: if project deletion failed.
401 """
402 try:
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100403 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)
406
407 if result.status_code != 204:
408 raise ClientException("Project was not deleted")
409
410 return True
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100411 except ClientException:
412 self.logger.exception("Error during project deletion using keystone")
413 raise AuthconnOperationException("Error during project deletion using Keystone")
414
415 def assign_role_to_user(self, user, project, role):
416 """
417 Assigning a role to a user in a project.
418
419 :param user: username.
420 :param project: project name.
421 :param role: role name.
422 :raises AuthconnOperationException: if role assignment failed.
423 """
424 try:
Eduardo Sousa44603902019-06-04 08:10:32 +0100425 if is_valid_uuid(user):
426 user_obj = self.keystone.users.get(user)
427 else:
428 user_obj = self.keystone.users.list(name=user)[0]
429
430 if is_valid_uuid(project):
431 project_obj = self.keystone.projects.get(project)
432 else:
433 project_obj = self.keystone.projects.list(name=project)[0]
434
435 if is_valid_uuid(role):
436 role_obj = self.keystone.roles.get(role)
437 else:
438 role_obj = self.keystone.roles.list(name=role)[0]
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100439
Eduardo Sousa29933fc2018-11-14 06:36:35 +0000440 self.keystone.roles.grant(role_obj, user=user_obj, project=project_obj)
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100441 except ClientException:
442 self.logger.exception("Error during user role assignment using keystone")
443 raise AuthconnOperationException("Error during user role assignment using Keystone")
444
445 def remove_role_from_user(self, user, project, role):
446 """
447 Remove a role from a user in a project.
448
449 :param user: username.
450 :param project: project name.
451 :param role: role name.
452 :raises AuthconnOperationException: if role assignment revocation failed.
453 """
454 try:
Eduardo Sousa29933fc2018-11-14 06:36:35 +0000455 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]
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100458
Eduardo Sousa29933fc2018-11-14 06:36:35 +0000459 self.keystone.roles.revoke(role_obj, user=user_obj, project=project_obj)
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100460 except ClientException:
461 self.logger.exception("Error during user role revocation using keystone")
462 raise AuthconnOperationException("Error during user role revocation using Keystone")