update initial status of NS and NSLCMOP
[osm/NBI.git] / osm_nbi / authconn_internal.py
1 # -*- coding: utf-8 -*-
2
3 # Copyright 2018 Telefonica S.A.
4 # Copyright 2018 ALTRAN InnovaciĆ³n S.L.
5 #
6 # Licensed under the Apache License, Version 2.0 (the "License"); you may
7 # not use this file except in compliance with the License. You may obtain
8 # a copy of the License at
9 #
10 # http://www.apache.org/licenses/LICENSE-2.0
11 #
12 # Unless required by applicable law or agreed to in writing, software
13 # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
14 # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
15 # License for the specific language governing permissions and limitations
16 # under the License.
17 #
18 # For those usages not covered by the Apache License, Version 2.0 please
19 # contact: esousa@whitestack.com or glavado@whitestack.com
20 ##
21
22 """
23 AuthconnInternal implements implements the connector for
24 OSM Internal Authentication Backend and leverages the RBAC model
25 """
26
27 __author__ = "Pedro de la Cruz Ramos <pdelacruzramos@altran.com>, " \
28 "Alfonso Tierno <alfonso.tiernosepulveda@telefoncia.com"
29 __date__ = "$06-jun-2019 11:16:08$"
30
31 from osm_nbi.authconn import Authconn, AuthException # , AuthconnOperationException
32 from osm_common.dbbase import DbException
33 from osm_nbi.base_topic import BaseTopic
34
35 import logging
36 import re
37 from time import time
38 from http import HTTPStatus
39 from uuid import uuid4
40 from hashlib import sha256
41 from copy import deepcopy
42 from random import choice as random_choice
43
44
45 class AuthconnInternal(Authconn):
46 def __init__(self, config, db, token_cache):
47 Authconn.__init__(self, config, db, token_cache)
48
49 self.logger = logging.getLogger("nbi.authenticator.internal")
50
51 self.db = db
52 self.token_cache = token_cache
53
54 # To be Confirmed
55 self.auth = None
56 self.sess = None
57
58 def validate_token(self, token):
59 """
60 Check if the token is valid.
61
62 :param token: token to validate
63 :return: dictionary with information associated with the token:
64 "_id": token id
65 "project_id": project id
66 "project_name": project name
67 "user_id": user id
68 "username": user name
69 "roles": list with dict containing {name, id}
70 "expires": expiration date
71 If the token is not valid an exception is raised.
72 """
73
74 try:
75 if not token:
76 raise AuthException("Needed a token or Authorization HTTP header", http_code=HTTPStatus.UNAUTHORIZED)
77
78 # try to get from cache first
79 now = time()
80 token_info = self.token_cache.get(token)
81 if token_info and token_info["expires"] < now:
82 # delete token. MUST be done with care, as another thread maybe already delete it. Do not use del
83 self.token_cache.pop(token, None)
84 token_info = None
85
86 # get from database if not in cache
87 if not token_info:
88 token_info = self.db.get_one("tokens", {"_id": token})
89 if token_info["expires"] < now:
90 raise AuthException("Expired Token or Authorization HTTP header", http_code=HTTPStatus.UNAUTHORIZED)
91
92 return token_info
93
94 except DbException as e:
95 if e.http_code == HTTPStatus.NOT_FOUND:
96 raise AuthException("Invalid Token or Authorization HTTP header", http_code=HTTPStatus.UNAUTHORIZED)
97 else:
98 raise
99 except AuthException:
100 raise
101 except Exception:
102 self.logger.exception("Error during token validation using internal backend")
103 raise AuthException("Error during token validation using internal backend",
104 http_code=HTTPStatus.UNAUTHORIZED)
105
106 def revoke_token(self, token):
107 """
108 Invalidate a token.
109
110 :param token: token to be revoked
111 """
112 try:
113 self.token_cache.pop(token, None)
114 self.db.del_one("tokens", {"_id": token})
115 return True
116 except DbException as e:
117 if e.http_code == HTTPStatus.NOT_FOUND:
118 raise AuthException("Token '{}' not found".format(token), http_code=HTTPStatus.NOT_FOUND)
119 else:
120 # raise
121 msg = "Error during token revocation using internal backend"
122 self.logger.exception(msg)
123 raise AuthException(msg, http_code=HTTPStatus.UNAUTHORIZED)
124
125 def authenticate(self, user, password, project=None, token_info=None):
126 """
127 Authenticate a user using username/password or previous token_info plus project; its creates a new token
128
129 :param user: user: name, id or None
130 :param password: password or None
131 :param project: name, id, or None. If None first found project will be used to get an scope token
132 :param token_info: previous token_info to obtain authorization
133 :param remote: remote host information
134 :return: the scoped token info or raises an exception. The token is a dictionary with:
135 _id: token string id,
136 username: username,
137 project_id: scoped_token project_id,
138 project_name: scoped_token project_name,
139 expires: epoch time when it expires,
140 """
141
142 now = time()
143 user_content = None
144
145 # Try using username/password
146 if user:
147 user_rows = self.db.get_list("users", {BaseTopic.id_field("users", user): user})
148 if user_rows:
149 user_content = user_rows[0]
150 salt = user_content["_admin"]["salt"]
151 shadow_password = sha256(password.encode('utf-8') + salt.encode('utf-8')).hexdigest()
152 if shadow_password != user_content["password"]:
153 user_content = None
154 if not user_content:
155 raise AuthException("Invalid username/password", http_code=HTTPStatus.UNAUTHORIZED)
156 elif token_info:
157 user_rows = self.db.get_list("users", {"username": token_info["username"]})
158 if user_rows:
159 user_content = user_rows[0]
160 else:
161 raise AuthException("Invalid token", http_code=HTTPStatus.UNAUTHORIZED)
162 else:
163 raise AuthException("Provide credentials: username/password or Authorization Bearer token",
164 http_code=HTTPStatus.UNAUTHORIZED)
165
166 token_id = ''.join(random_choice('abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789')
167 for _ in range(0, 32))
168
169 # projects = user_content.get("projects", [])
170 prm_list = user_content.get("project_role_mappings", [])
171
172 if not project:
173 project = prm_list[0]["project"] if prm_list else None
174 if not project:
175 raise AuthException("can't find a default project for this user", http_code=HTTPStatus.UNAUTHORIZED)
176
177 projects = [prm["project"] for prm in prm_list]
178
179 proj = self.db.get_one("projects", {BaseTopic.id_field("projects", project): project})
180 project_name = proj["name"]
181 project_id = proj["_id"]
182 if project_name not in projects and project_id not in projects:
183 raise AuthException("project {} not allowed for this user".format(project),
184 http_code=HTTPStatus.UNAUTHORIZED)
185
186 # TODO remove admin, this vill be used by roles RBAC
187 if project_name == "admin":
188 token_admin = True
189 else:
190 token_admin = proj.get("admin", False)
191
192 # add token roles
193 roles = []
194 roles_list = []
195 for prm in prm_list:
196 if prm["project"] in [project_id, project_name]:
197 role = self.db.get_one("roles", {BaseTopic.id_field("roles", prm["role"]): prm["role"]})
198 rid = role["_id"]
199 if rid not in roles:
200 rnm = role["name"]
201 roles.append(rid)
202 roles_list.append({"name": rnm, "id": rid})
203 if not roles_list:
204 rid = self.db.get_one("roles", {"name": "project_admin"})["_id"]
205 roles_list = [{"name": "project_admin", "id": rid}]
206
207 new_token = {"issued_at": now,
208 "expires": now + 3600,
209 "_id": token_id,
210 "id": token_id,
211 "project_id": proj["_id"],
212 "project_name": proj["name"],
213 "username": user_content["username"],
214 "user_id": user_content["_id"],
215 "admin": token_admin,
216 "roles": roles_list,
217 }
218
219 self.token_cache[token_id] = new_token
220 self.db.create("tokens", new_token)
221 return deepcopy(new_token)
222
223 def get_role_list(self, filter_q={}):
224 """
225 Get role list.
226
227 :return: returns the list of roles.
228 """
229 return self.db.get_list("roles", filter_q)
230
231 def create_role(self, role_info):
232 """
233 Create a role.
234
235 :param role_info: full role info.
236 :return: returns the role id.
237 :raises AuthconnOperationException: if role creation failed.
238 """
239 # TODO: Check that role name does not exist ?
240 rid = str(uuid4())
241 role_info["_id"] = rid
242 rid = self.db.create("roles", role_info)
243 return rid
244
245 def delete_role(self, role_id):
246 """
247 Delete a role.
248
249 :param role_id: role identifier.
250 :raises AuthconnOperationException: if role deletion failed.
251 """
252 return self.db.del_one("roles", {"_id": role_id})
253
254 def update_role(self, role_info):
255 """
256 Update a role.
257
258 :param role_info: full role info.
259 :return: returns the role name and id.
260 :raises AuthconnOperationException: if user creation failed.
261 """
262 rid = role_info["_id"]
263 self.db.set_one("roles", {"_id": rid}, role_info) # CONFIRM
264 return {"_id": rid, "name": role_info["name"]}
265
266 def create_user(self, user_info):
267 """
268 Create a user.
269
270 :param user_info: full user info.
271 :return: returns the username and id of the user.
272 """
273 BaseTopic.format_on_new(user_info, make_public=False)
274 salt = uuid4().hex
275 user_info["_admin"]["salt"] = salt
276 if "password" in user_info:
277 user_info["password"] = sha256(user_info["password"].encode('utf-8') + salt.encode('utf-8')).hexdigest()
278 # "projects" are not stored any more
279 if "projects" in user_info:
280 del user_info["projects"]
281 self.db.create("users", user_info)
282 return {"username": user_info["username"], "_id": user_info["_id"]}
283
284 def update_user(self, user_info):
285 """
286 Change the user name and/or password.
287
288 :param user_info: user info modifications
289 """
290 uid = user_info["_id"]
291 user_data = self.db.get_one("users", {BaseTopic.id_field("users", uid): uid})
292 BaseTopic.format_on_edit(user_data, user_info)
293 # User Name
294 usnm = user_info.get("username")
295 if usnm:
296 user_data["username"] = usnm
297 # If password is given and is not already encripted
298 pswd = user_info.get("password")
299 if pswd and (len(pswd) != 64 or not re.match('[a-fA-F0-9]*', pswd)): # TODO: Improve check?
300 salt = uuid4().hex
301 if "_admin" not in user_data:
302 user_data["_admin"] = {}
303 user_data["_admin"]["salt"] = salt
304 user_data["password"] = sha256(pswd.encode('utf-8') + salt.encode('utf-8')).hexdigest()
305 # Project-Role Mappings
306 # TODO: Check that user_info NEVER includes "project_role_mappings"
307 if "project_role_mappings" not in user_data:
308 user_data["project_role_mappings"] = []
309 for prm in user_info.get("add_project_role_mappings", []):
310 user_data["project_role_mappings"].append(prm)
311 for prm in user_info.get("remove_project_role_mappings", []):
312 for pidf in ["project", "project_name"]:
313 for ridf in ["role", "role_name"]:
314 try:
315 user_data["project_role_mappings"].remove({"role": prm[ridf], "project": prm[pidf]})
316 except KeyError:
317 pass
318 except ValueError:
319 pass
320 idf = BaseTopic.id_field("users", uid)
321 self.db.set_one("users", {idf: uid}, user_data)
322 if user_info.get("remove_project_role_mappings"):
323 self.db.del_list("tokens", {"user_id" if idf == "_id" else idf: uid})
324 self.token_cache.clear()
325
326 def delete_user(self, user_id):
327 """
328 Delete user.
329
330 :param user_id: user identifier.
331 :raises AuthconnOperationException: if user deletion failed.
332 """
333 self.db.del_one("users", {"_id": user_id})
334 self.db.del_list("tokens", {"user_id": user_id})
335 self.token_cache.clear()
336 return True
337
338 def get_user_list(self, filter_q=None):
339 """
340 Get user list.
341
342 :param filter_q: dictionary to filter user list by name (username is also admited) and/or _id
343 :return: returns a list of users.
344 """
345 filt = filter_q or {}
346 if "name" in filt:
347 filt["username"] = filt["name"]
348 del filt["name"]
349 users = self.db.get_list("users", filt)
350 project_id_name = {}
351 role_id_name = {}
352 for user in users:
353 prms = user.get("project_role_mappings")
354 projects = user.get("projects")
355 if prms:
356 projects = []
357 # add project_name and role_name. Generate projects for backward compatibility
358 for prm in prms:
359 project_id = prm["project"]
360 if project_id not in project_id_name:
361 pr = self.db.get_one("projects", {BaseTopic.id_field("projects", project_id): project_id},
362 fail_on_empty=False)
363 project_id_name[project_id] = pr["name"] if pr else None
364 prm["project_name"] = project_id_name[project_id]
365 if prm["project_name"] not in projects:
366 projects.append(prm["project_name"])
367
368 role_id = prm["role"]
369 if role_id not in role_id_name:
370 role = self.db.get_one("roles", {BaseTopic.id_field("roles", role_id): role_id},
371 fail_on_empty=False)
372 role_id_name[role_id] = role["name"] if role else None
373 prm["role_name"] = role_id_name[role_id]
374 user["projects"] = projects # for backward compatibility
375 elif projects:
376 # user created with an old version. Create a project_role mapping with role project_admin
377 user["project_role_mappings"] = []
378 role = self.db.get_one("roles", {BaseTopic.id_field("roles", "project_admin"): "project_admin"})
379 for p_id_name in projects:
380 pr = self.db.get_one("projects", {BaseTopic.id_field("projects", p_id_name): p_id_name})
381 prm = {"project": pr["_id"],
382 "project_name": pr["name"],
383 "role_name": "project_admin",
384 "role": role["_id"]
385 }
386 user["project_role_mappings"].append(prm)
387 else:
388 user["projects"] = []
389 user["project_role_mappings"] = []
390
391 return users
392
393 def get_project_list(self, filter_q={}):
394 """
395 Get role list.
396
397 :return: returns the list of projects.
398 """
399 return self.db.get_list("projects", filter_q)
400
401 def create_project(self, project_info):
402 """
403 Create a project.
404
405 :param project: full project info.
406 :return: the internal id of the created project
407 :raises AuthconnOperationException: if project creation failed.
408 """
409 pid = self.db.create("projects", project_info)
410 return pid
411
412 def delete_project(self, project_id):
413 """
414 Delete a project.
415
416 :param project_id: project identifier.
417 :raises AuthconnOperationException: if project deletion failed.
418 """
419 filter_q = {BaseTopic.id_field("projects", project_id): project_id}
420 r = self.db.del_one("projects", filter_q)
421 return r
422
423 def update_project(self, project_id, project_info):
424 """
425 Change the name of a project
426
427 :param project_id: project to be changed
428 :param project_info: full project info
429 :return: None
430 :raises AuthconnOperationException: if project update failed.
431 """
432 self.db.set_one("projects", {BaseTopic.id_field("projects", project_id): project_id}, project_info)