Fix bug 732
[osm/NBI.git] / osm_nbi / authconn_keystone.py
1 # -*- coding: utf-8 -*-
2
3 # 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
21 """
22 AuthconnKeystone implements implements the connector for
23 Openstack Keystone and leverages the RBAC model, to bring
24 it for OSM.
25 """
26
27
28 __author__ = "Eduardo Sousa <esousa@whitestack.com>"
29 __date__ = "$27-jul-2018 23:59:59$"
30
31 from authconn import Authconn, AuthException, AuthconnOperationException
32
33 import logging
34 import requests
35 import time
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
43
44
45 class 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
58 # 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
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)
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
119 new_token = self.keystone.get_raw_token_from_identity_service(
120 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
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)
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:
157 self.logger.info("Revoking token: " + token)
158 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
165 def get_user_project_list(self, token):
166 """
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
182 def get_user_role_list(self, token):
183 """
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)
192 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]
195
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.
208 :return: returns the id of the user in keystone.
209 """
210 try:
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")
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:
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")
231
232 def delete_user(self, user_id):
233 """
234 Delete user.
235
236 :param user_id: user identifier.
237 :raises AuthconnOperationException: if user deletion failed.
238 """
239 try:
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)
243
244 if result.status_code != 204:
245 raise ClientException("User was not deleted")
246
247 return True
248 except ClientException:
249 self.logger.exception("Error during user deletion using keystone")
250 raise AuthconnOperationException("Error during user deletion using Keystone")
251
252 def get_user_list(self, filter_q={}):
253 """
254 Get user list.
255
256 :param filter_q: dictionary to filter user list.
257 :return: returns a list of users.
258 """
259 try:
260 users = self.keystone.users.list()
261 users = [{
262 "username": user.name,
263 "_id": user.id,
264 "id": user.id
265 } for user in users if user.name != self.admin_username]
266
267 allowed_fields = ["_id", "id", "username"]
268 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
275 for user in users:
276 projects = self.keystone.projects.list(user=user["_id"])
277 projects = [{
278 "name": project.name,
279 "_id": project.id,
280 "id": project.id
281 } 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,
287 "_id": role.id,
288 "id": role.id
289 } 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
303 :return: returns the list of roles.
304 """
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
318 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:
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")
333
334 def delete_role(self, role_id):
335 """
336 Delete a role.
337
338 :param role_id: role identifier.
339 :raises AuthconnOperationException: if role deletion failed.
340 """
341 try:
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)
345
346 if result.status_code != 204:
347 raise ClientException("Role was not deleted")
348
349 return True
350 except ClientException:
351 self.logger.exception("Error during role deletion using keystone")
352 raise AuthconnOperationException("Error during role deletion using Keystone")
353
354 def get_project_list(self, filter_q={}):
355 """
356 Get all the projects.
357
358 :param filter_q: dictionary to filter project list.
359 :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
368 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
376 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
381 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:
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")
394
395 def delete_project(self, project_id):
396 """
397 Delete a project.
398
399 :param project_id: project identifier.
400 :raises AuthconnOperationException: if project deletion failed.
401 """
402 try:
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)
406
407 if result.status_code != 204:
408 raise ClientException("Project was not deleted")
409
410 return True
411 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:
425 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]
439
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")
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:
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]
458
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")