5e35e8f481b90a7500979ee411a2b2f394ffe0c2
[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 __date__ = "$06-jun-2019 11:16:08$"
29
30 from authconn import Authconn, AuthException
31 from osm_common.dbbase import DbException
32 from base_topic import BaseTopic
33
34 import logging
35 from time import time
36 from http import HTTPStatus
37 from uuid import uuid4
38 from hashlib import sha256
39 from copy import deepcopy
40 from random import choice as random_choice
41
42
43 class AuthconnInternal(Authconn):
44 def __init__(self, config, db, token_cache):
45 Authconn.__init__(self, config)
46
47 self.logger = logging.getLogger("nbi.authenticator.internal")
48
49 # Get Configuration
50 # self.xxx = config.get("xxx", "default")
51
52 self.db = db
53 self.token_cache = token_cache
54
55 # To be Confirmed
56 self.auth = None
57 self.sess = None
58
59 # def create_token (self, user, password, projects=[], project=None, remote=None):
60 # Not Required
61
62 # def authenticate_with_user_password(self, user, password, project=None, remote=None):
63 # Not Required
64
65 # def authenticate_with_token(self, token, project=None, remote=None):
66 # Not Required
67
68 # def get_user_project_list(self, token):
69 # Not Required
70
71 # def get_user_role_list(self, token):
72 # Not Required
73
74 # def create_user(self, user, password):
75 # Not Required
76
77 # def change_password(self, user, new_password):
78 # Not Required
79
80 # def delete_user(self, user_id):
81 # Not Required
82
83 # def get_user_list(self, filter_q={}):
84 # Not Required
85
86 # def get_project_list(self, filter_q={}):
87 # Not required
88
89 # def create_project(self, project):
90 # Not required
91
92 # def delete_project(self, project_id):
93 # Not required
94
95 # def assign_role_to_user(self, user, project, role):
96 # Not required in Phase 1
97
98 # def remove_role_from_user(self, user, project, role):
99 # Not required in Phase 1
100
101 def validate_token(self, token):
102 """
103 Check if the token is valid.
104
105 :param token: token to validate
106 :return: dictionary with information associated with the token:
107 "_id": token id
108 "project_id": project id
109 "project_name": project name
110 "user_id": user id
111 "username": user name
112 "roles": list with dict containing {name, id}
113 "expires": expiration date
114 If the token is not valid an exception is raised.
115 """
116
117 try:
118 if not token:
119 raise AuthException("Needed a token or Authorization HTTP header", http_code=HTTPStatus.UNAUTHORIZED)
120
121 # try to get from cache first
122 now = time()
123 session = self.token_cache.get(token)
124 if session and session["expires"] < now:
125 # delete token. MUST be done with care, as another thread maybe already delete it. Do not use del
126 self.token_cache.pop(token, None)
127 session = None
128
129 # get from database if not in cache
130 if not session:
131 session = self.db.get_one("tokens", {"_id": token})
132 if session["expires"] < now:
133 raise AuthException("Expired Token or Authorization HTTP header", http_code=HTTPStatus.UNAUTHORIZED)
134
135 # complete token information
136 pid = session["project_id"]
137 prj = self.db.get_one("projects", {BaseTopic.id_field("projects", pid): pid})
138 session["project_id"] = prj["_id"]
139 session["project_name"] = prj["name"]
140 session["user_id"] = self.db.get_one("users", {"username": session["username"]})["_id"]
141
142 # add token roles - PROVISIONAL
143 role_id = self.db.get_one("roles", {"name": "system_admin"})["_id"]
144 session["roles"] = [{"name": "system_admin", "id": role_id}]
145
146 return session
147
148 except DbException as e:
149 if e.http_code == HTTPStatus.NOT_FOUND:
150 raise AuthException("Invalid Token or Authorization HTTP header", http_code=HTTPStatus.UNAUTHORIZED)
151 else:
152 raise
153 except AuthException:
154 if self.config["global"].get("test.user_not_authorized"):
155 return {"id": "fake-token-id-for-test",
156 "project_id": self.config["global"].get("test.project_not_authorized", "admin"),
157 "username": self.config["global"]["test.user_not_authorized"], "admin": True}
158 else:
159 raise
160 except Exception:
161 self.logger.exception("Error during token validation using internal backend")
162 raise AuthException("Error during token validation using internal backend",
163 http_code=HTTPStatus.UNAUTHORIZED)
164
165 def revoke_token(self, token):
166 """
167 Invalidate a token.
168
169 :param token: token to be revoked
170 """
171 try:
172 self.token_cache.pop(token, None)
173 self.db.del_one("tokens", {"_id": token})
174 return True
175 except DbException as e:
176 if e.http_code == HTTPStatus.NOT_FOUND:
177 raise AuthException("Token '{}' not found".format(token), http_code=HTTPStatus.NOT_FOUND)
178 else:
179 # raise
180 msg = "Error during token revocation using internal backend"
181 self.logger.exception(msg)
182 raise AuthException(msg, http_code=HTTPStatus.UNAUTHORIZED)
183
184 def authenticate(self, user, password, project=None, token=None):
185 """
186 Authenticate a user using username/password or token, plus project
187
188 :param user: user: name, id or None
189 :param password: password or None
190 :param project: name, id, or None. If None first found project will be used to get an scope token
191 :param token: previous token to obtain authorization
192 :param remote: remote host information
193 :return: the scoped token info or raises an exception. The token is a dictionary with:
194 _id: token string id,
195 username: username,
196 project_id: scoped_token project_id,
197 project_name: scoped_token project_name,
198 expires: epoch time when it expires,
199 """
200
201 now = time()
202 user_content = None
203
204 try:
205 # Try using username/password
206 if user:
207 user_rows = self.db.get_list("users", {BaseTopic.id_field("users", user): user})
208 if user_rows:
209 user_content = user_rows[0]
210 salt = user_content["_admin"]["salt"]
211 shadow_password = sha256(password.encode('utf-8') + salt.encode('utf-8')).hexdigest()
212 if shadow_password != user_content["password"]:
213 user_content = None
214 if not user_content:
215 raise AuthException("Invalid username/password", http_code=HTTPStatus.UNAUTHORIZED)
216 elif token:
217 user_rows = self.db.get_list("users", {"username": token["username"]})
218 if user_rows:
219 user_content = user_rows[0]
220 else:
221 raise AuthException("Invalid token", http_code=HTTPStatus.UNAUTHORIZED)
222 else:
223 raise AuthException("Provide credentials: username/password or Authorization Bearer token",
224 http_code=HTTPStatus.UNAUTHORIZED)
225
226 token_id = ''.join(random_choice('abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789')
227 for _ in range(0, 32))
228 project_id = project
229
230 if project_id:
231 if project_id != "admin":
232 # To allow project names in project_id
233 proj = self.db.get_one("projects", {BaseTopic.id_field("projects", project_id): project_id})
234 if proj["_id"] not in user_content["projects"] and proj["name"] not in user_content["projects"]:
235 raise AuthException("project {} not allowed for this user"
236 .format(project_id), http_code=HTTPStatus.UNAUTHORIZED)
237 else:
238 project_id = user_content["projects"][0]
239
240 if project_id == "admin":
241 token_admin = True
242 else:
243 # To allow project names in project_id
244 proj = self.db.get_one("projects", {BaseTopic.id_field("projects", project_id): project_id})
245 token_admin = proj.get("admin", False)
246
247 new_token = {"issued_at": now, "expires": now + 3600,
248 "_id": token_id, "id": token_id,
249 "project_id": project_id,
250 "username": user_content["username"],
251 "admin": token_admin}
252
253 self.token_cache[token_id] = new_token
254 self.db.create("tokens", new_token)
255 # self._internal_tokens_prune(now) # Belongs to Authenticator - REMOVE?
256 return deepcopy(new_token)
257
258 except Exception as e:
259 msg = "Error during user authentication using internal backend: {}".format(e)
260 self.logger.exception(msg)
261 raise AuthException(msg, http_code=HTTPStatus.UNAUTHORIZED)
262
263 def get_role_list(self):
264 """
265 Get role list.
266
267 :return: returns the list of roles.
268 """
269 try:
270 role_list = self.db.get_list("roles")
271 roles = [{"name": role["name"], "_id": role["_id"]} for role in role_list] # if role.name != "service" ?
272 return roles
273 except Exception:
274 raise AuthException("Error during role listing using internal backend", http_code=HTTPStatus.UNAUTHORIZED)
275
276 def create_role(self, role):
277 """
278 Create a role.
279
280 :param role: role name.
281 :raises AuthconnOperationException: if role creation failed.
282 """
283 # try:
284 # TODO: Check that role name does not exist ?
285 return str(uuid4())
286 # except Exception:
287 # raise AuthconnOperationException("Error during role creation using internal backend")
288 # except Conflict as ex:
289 # self.logger.info("Duplicate entry: %s", str(ex))
290
291 def delete_role(self, role_id):
292 """
293 Delete a role.
294
295 :param role_id: role identifier.
296 :raises AuthconnOperationException: if role deletion failed.
297 """
298 # try:
299 # TODO: Check that role exists ?
300 return True
301 # except Exception:
302 # raise AuthconnOperationException("Error during role deletion using internal backend")