fix bug 749 750: Returns a proper Location Header for project-create
[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 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)
104
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
120 new_token = self.keystone.get_raw_token_from_identity_service(
121 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
127 return new_token["auth_token"], project_names
128 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)
132
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
148 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)
152
153 def revoke_token(self, token):
154 """
155 Invalidate a token.
156
157 :param token: token to be revoked
158 """
159 try:
160 self.logger.info("Revoking token: " + token)
161 self.keystone.tokens.revoke_token(token=token)
162
163 return True
164 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)
168
169 def get_user_project_list(self, token):
170 """
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
182 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)
186
187 def get_user_role_list(self, token):
188 """
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)
197 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]
200
201 return roles
202 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)
206
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.
214 :return: returns the id of the user in keystone.
215 """
216 try:
217 new_user = self.keystone.users.create(user, password=password, domain=self.user_domain_name)
218 return {"username": new_user.name, "_id": new_user.id}
219 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))
222
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:
232 user_obj = list(filter(lambda x: x.name == user, self.keystone.users.list()))[0]
233 self.keystone.users.update(user_obj, password=new_password)
234 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))
237
238 def delete_user(self, user_id):
239 """
240 Delete user.
241
242 :param user_id: user identifier.
243 :raises AuthconnOperationException: if user deletion failed.
244 """
245 try:
246 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
254 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))
257
258 def get_user_list(self, filter_q={}):
259 """
260 Get user list.
261
262 :param filter_q: dictionary to filter user list.
263 :return: returns a list of users.
264 """
265 try:
266 users = self.keystone.users.list()
267 users = [{
268 "username": user.name,
269 "_id": user.id,
270 "id": user.id
271 } for user in users if user.name != self.admin_username]
272
273 allowed_fields = ["_id", "id", "username"]
274 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
281 for user in users:
282 projects = self.keystone.projects.list(user=user["_id"])
283 projects = [{
284 "name": project.name,
285 "_id": project.id,
286 "id": project.id
287 } 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,
293 "_id": role.id,
294 "id": role.id
295 } for role in roles]
296 project["roles"] = roles
297
298 user["projects"] = projects
299
300 return users
301 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))
304
305 def get_role_list(self):
306 """
307 Get role list.
308
309 :return: returns the list of roles.
310 """
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
320 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)
324
325 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:
333 result = self.keystone.roles.create(role)
334 return {"name": result.name, "_id": result.id}
335 except Conflict as ex:
336 self.logger.info("Duplicate entry: %s", str(ex))
337 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))
340
341 def delete_role(self, role_id):
342 """
343 Delete a role.
344
345 :param role_id: role identifier.
346 :raises AuthconnOperationException: if role deletion failed.
347 """
348 try:
349 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
357 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))
360
361 def get_project_list(self, filter_q={}):
362 """
363 Get all the projects.
364
365 :param filter_q: dictionary to filter project list.
366 :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
375 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
383 return projects
384 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)
388
389 def create_project(self, project):
390 """
391 Create a project.
392
393 :param project: project name.
394 :return: the internal id of the created project
395 :raises AuthconnOperationException: if project creation failed.
396 """
397 try:
398 result = self.keystone.projects.create(project, self.project_domain_name)
399 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))
403
404 def delete_project(self, project_id):
405 """
406 Delete a project.
407
408 :param project_id: project identifier.
409 :raises AuthconnOperationException: if project deletion failed.
410 """
411 try:
412 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
420 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))
436
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:
447 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]
461
462 self.keystone.roles.grant(role_obj, user=user_obj, project=project_obj)
463 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))
466
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:
477 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]
480
481 self.keystone.roles.revoke(role_obj, user=user_obj, project=project_obj)
482 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))