fix bug 748: provide a proper error when user is not valid upon new token
[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(self, user, password, project=None, token=None):
81 """
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:
88 _id: token string id,
89 username: username,
90 project_id: scoped_token project_id,
91 project_name: scoped_token project_name,
92 expires: epoch time when it expires,
93
94 """
95 try:
96 username = None
97 user_id = None
98 project_id = None
99 project_name = None
100
101 if user:
102 if is_valid_uuid(user):
103 user_id = user
104 else:
105 username = user
106
107 # get an unscoped token firstly
108 unscoped_token = self.keystone.get_raw_token_from_identity_service(
109 auth_url=self.auth_url,
110 user_id=user_id,
111 username=username,
112 password=password,
113 user_domain_name=self.user_domain_name,
114 project_domain_name=self.project_domain_name)
115 elif token:
116 unscoped_token = self.keystone.tokens.validate(token=token)
117 else:
118 raise AuthException("Provide credentials: username/password or Authorization Bearer token",
119 http_code=HTTPStatus.UNAUTHORIZED)
120
121 if not project:
122 # get first project for the user
123 project_list = self.keystone.projects.list(user=unscoped_token["user"]["id"])
124 if not project_list:
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
128 else:
129 if is_valid_uuid(project):
130 project_id = project
131 else:
132 project_name = project
133
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"])
141
142 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(),
148 }
149
150 return auth_token
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)
155
156 # def authenticate_with_token(self, token, project=None):
157 # """
158 # Authenticate a user using a token. Can be used to revalidate the token
159 # or to get a scoped token.
160 #
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.
165 # """
166 # try:
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]
170 #
171 # new_token = self.keystone.get_raw_token_from_identity_service(
172 # auth_url=self.auth_url,
173 # token=token,
174 # project_name=project,
175 # project_id=None,
176 # user_domain_name=self.user_domain_name,
177 # project_domain_name=self.project_domain_name)
178 #
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)
184
185 def validate_token(self, token):
186 """
187 Check if the token is valid.
188
189 :param token: token to validate
190 :return: dictionary with information associated with the token. If the
191 token is not valid, returns None.
192 """
193 if not token:
194 return
195
196 try:
197 token_info = self.keystone.tokens.validate(token=token)
198
199 return token_info
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)
204
205 def revoke_token(self, token):
206 """
207 Invalidate a token.
208
209 :param token: token to be revoked
210 """
211 try:
212 self.logger.info("Revoking token: " + token)
213 self.keystone.tokens.revoke_token(token=token)
214
215 return True
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)
220
221 def get_user_project_list(self, token):
222 """
223 Get all the projects associated with a user.
224
225 :param token: valid token
226 :return: list of projects
227 """
228 try:
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]
232
233 return project_names
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)
238
239 def get_user_role_list(self, token):
240 """
241 Get role list for a scoped project.
242
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.
246 """
247 try:
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"])
250
251 roles = [role.name for role in roles_info]
252
253 return roles
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)
258
259 def create_user(self, user, password):
260 """
261 Create a user.
262
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.
267 """
268 try:
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))
274
275 def change_password(self, user, new_password):
276 """
277 Change the user password.
278
279 :param user: username.
280 :param new_password: new password.
281 :raises AuthconnOperationException: if user password change failed.
282 """
283 try:
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))
289
290 def delete_user(self, user_id):
291 """
292 Delete user.
293
294 :param user_id: user identifier.
295 :raises AuthconnOperationException: if user deletion failed.
296 """
297 try:
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)
301
302 result, detail = self.keystone.users.delete(user_id)
303 if result.status_code != 204:
304 raise ClientException("error {} {}".format(result.status_code, detail))
305
306 return True
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))
310
311 def get_user_list(self, filter_q={}):
312 """
313 Get user list.
314
315 :param filter_q: dictionary to filter user list.
316 :return: returns a list of users.
317 """
318 try:
319 users = self.keystone.users.list()
320 users = [{
321 "username": user.name,
322 "_id": user.id,
323 "id": user.id
324 } for user in users if user.name != self.admin_username]
325
326 allowed_fields = ["_id", "id", "username"]
327 for key in filter_q.keys():
328 if key not in allowed_fields:
329 continue
330
331 users = [user for user in users
332 if filter_q[key] == user[key]]
333
334 for user in users:
335 projects = self.keystone.projects.list(user=user["_id"])
336 projects = [{
337 "name": project.name,
338 "_id": project.id,
339 "id": project.id
340 } for project in projects]
341
342 for project in projects:
343 roles = self.keystone.roles.list(user=user["_id"], project=project["_id"])
344 roles = [{
345 "name": role.name,
346 "_id": role.id,
347 "id": role.id
348 } for role in roles]
349 project["roles"] = roles
350
351 user["projects"] = projects
352
353 return users
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))
357
358 def get_role_list(self):
359 """
360 Get role list.
361
362 :return: returns the list of roles.
363 """
364 try:
365 roles_list = self.keystone.roles.list()
366
367 roles = [{
368 "name": role.name,
369 "_id": role.id
370 } for role in roles_list if role.name != "service"]
371
372 return roles
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)
377
378 def create_role(self, role):
379 """
380 Create a role.
381
382 :param role: role name.
383 :raises AuthconnOperationException: if role creation failed.
384 """
385 try:
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))
393
394 def delete_role(self, role_id):
395 """
396 Delete a role.
397
398 :param role_id: role identifier.
399 :raises AuthconnOperationException: if role deletion failed.
400 """
401 try:
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)
405
406 if result.status_code != 204:
407 raise ClientException("error {} {}".format(result.status_code, detail))
408
409 return True
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))
413
414 def get_project_list(self, filter_q=None):
415 """
416 Get all the projects.
417
418 :param filter_q: dictionary to filter project list.
419 :return: list of projects
420 """
421 try:
422 filter_name = None
423 if filter_q:
424 filter_name = filter_q.get("name")
425 projects = self.keystone.projects.list(name=filter_name)
426
427 projects = [{
428 "name": project.name,
429 "_id": project.id
430 } for project in projects]
431
432 if filter_q and filter_q.get("_id"):
433 projects = [project for project in projects
434 if filter_q["_id"] == project["_id"]]
435
436 return projects
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)
441
442 def create_project(self, project):
443 """
444 Create a project.
445
446 :param project: project name.
447 :return: the internal id of the created project
448 :raises AuthconnOperationException: if project creation failed.
449 """
450 try:
451 result = self.keystone.projects.create(project, self.project_domain_name)
452 return result.id
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))
456
457 def delete_project(self, project_id):
458 """
459 Delete a project.
460
461 :param project_id: project identifier.
462 :raises AuthconnOperationException: if project deletion failed.
463 """
464 try:
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)
468
469 result, detail = self.keystone.projects.delete(project_id)
470 if result.status_code != 204:
471 raise ClientException("error {} {}".format(result.status_code, detail))
472
473 return True
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))
477
478 def update_project(self, project_id, new_name):
479 """
480 Change the name of a project
481 :param project_id: project to be changed
482 :param new_name: new name
483 :return: None
484 """
485 try:
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))
490
491 def assign_role_to_user(self, user, project, role):
492 """
493 Assigning a role to a user in a project.
494
495 :param user: username.
496 :param project: project name.
497 :param role: role name.
498 :raises AuthconnOperationException: if role assignment failed.
499 """
500 try:
501 if is_valid_uuid(user):
502 user_obj = self.keystone.users.get(user)
503 else:
504 user_obj = self.keystone.users.list(name=user)[0]
505
506 if is_valid_uuid(project):
507 project_obj = self.keystone.projects.get(project)
508 else:
509 project_obj = self.keystone.projects.list(name=project)[0]
510
511 if is_valid_uuid(role):
512 role_obj = self.keystone.roles.get(role)
513 else:
514 role_obj = self.keystone.roles.list(name=role)[0]
515
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))
520
521 def remove_role_from_user(self, user, project, role):
522 """
523 Remove a role from a user in a project.
524
525 :param user: username.
526 :param project: project name.
527 :param role: role name.
528 :raises AuthconnOperationException: if role assignment revocation failed.
529 """
530 try:
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]
534
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))