Register operations for VIM, WIM, SDNC
[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 token_info = self.token_cache.get(token)
124 if token_info and token_info["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 token_info = None
128
129 # get from database if not in cache
130 if not token_info:
131 token_info = self.db.get_one("tokens", {"_id": token})
132 if token_info["expires"] < now:
133 raise AuthException("Expired Token or Authorization HTTP header", http_code=HTTPStatus.UNAUTHORIZED)
134
135 return token_info
136
137 except DbException as e:
138 if e.http_code == HTTPStatus.NOT_FOUND:
139 raise AuthException("Invalid Token or Authorization HTTP header", http_code=HTTPStatus.UNAUTHORIZED)
140 else:
141 raise
142 except AuthException:
143 if self.config["global"].get("test.user_not_authorized"):
144 return {"id": "fake-token-id-for-test",
145 "project_id": self.config["global"].get("test.project_not_authorized", "admin"),
146 "username": self.config["global"]["test.user_not_authorized"], "admin": True}
147 else:
148 raise
149 except Exception:
150 self.logger.exception("Error during token validation using internal backend")
151 raise AuthException("Error during token validation using internal backend",
152 http_code=HTTPStatus.UNAUTHORIZED)
153
154 def revoke_token(self, token):
155 """
156 Invalidate a token.
157
158 :param token: token to be revoked
159 """
160 try:
161 self.token_cache.pop(token, None)
162 self.db.del_one("tokens", {"_id": token})
163 return True
164 except DbException as e:
165 if e.http_code == HTTPStatus.NOT_FOUND:
166 raise AuthException("Token '{}' not found".format(token), http_code=HTTPStatus.NOT_FOUND)
167 else:
168 # raise
169 msg = "Error during token revocation using internal backend"
170 self.logger.exception(msg)
171 raise AuthException(msg, http_code=HTTPStatus.UNAUTHORIZED)
172
173 def authenticate(self, user, password, project=None, token_info=None):
174 """
175 Authenticate a user using username/password or previous token_info plus project; its creates a new token
176
177 :param user: user: name, id or None
178 :param password: password or None
179 :param project: name, id, or None. If None first found project will be used to get an scope token
180 :param token_info: previous token_info to obtain authorization
181 :param remote: remote host information
182 :return: the scoped token info or raises an exception. The token is a dictionary with:
183 _id: token string id,
184 username: username,
185 project_id: scoped_token project_id,
186 project_name: scoped_token project_name,
187 expires: epoch time when it expires,
188 """
189
190 now = time()
191 user_content = None
192
193 try:
194 # Try using username/password
195 if user:
196 user_rows = self.db.get_list("users", {BaseTopic.id_field("users", user): user})
197 if user_rows:
198 user_content = user_rows[0]
199 salt = user_content["_admin"]["salt"]
200 shadow_password = sha256(password.encode('utf-8') + salt.encode('utf-8')).hexdigest()
201 if shadow_password != user_content["password"]:
202 user_content = None
203 if not user_content:
204 raise AuthException("Invalid username/password", http_code=HTTPStatus.UNAUTHORIZED)
205 elif token_info:
206 user_rows = self.db.get_list("users", {"username": token_info["username"]})
207 if user_rows:
208 user_content = user_rows[0]
209 else:
210 raise AuthException("Invalid token", http_code=HTTPStatus.UNAUTHORIZED)
211 else:
212 raise AuthException("Provide credentials: username/password or Authorization Bearer token",
213 http_code=HTTPStatus.UNAUTHORIZED)
214
215 token_id = ''.join(random_choice('abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789')
216 for _ in range(0, 32))
217
218 # TODO when user contained project_role_mappings with project_id,project_ name this checking to
219 # database will not be needed
220 if not project:
221 project = user_content["projects"][0]
222
223 # To allow project names in project_id
224 proj = self.db.get_one("projects", {BaseTopic.id_field("projects", project): project})
225 if proj["_id"] not in user_content["projects"] and proj["name"] not in user_content["projects"]:
226 raise AuthException("project {} not allowed for this user".format(project),
227 http_code=HTTPStatus.UNAUTHORIZED)
228
229 # TODO remove admin, this vill be used by roles RBAC
230 if proj["name"] == "admin":
231 token_admin = True
232 else:
233 token_admin = proj.get("admin", False)
234
235 # TODO add token roles - PROVISIONAL. Get this list from user_content["project_role_mappings"]
236 role_id = self.db.get_one("roles", {"name": "system_admin"})["_id"]
237 roles_list = [{"name": "system_admin", "id": role_id}]
238
239 new_token = {"issued_at": now,
240 "expires": now + 3600,
241 "_id": token_id,
242 "id": token_id,
243 "project_id": proj["_id"],
244 "project_name": proj["name"],
245 "username": user_content["username"],
246 "user_id": user_content["_id"],
247 "admin": token_admin,
248 "roles": roles_list,
249 }
250
251 self.token_cache[token_id] = new_token
252 self.db.create("tokens", new_token)
253 return deepcopy(new_token)
254
255 except Exception as e:
256 msg = "Error during user authentication using internal backend: {}".format(e)
257 self.logger.exception(msg)
258 raise AuthException(msg, http_code=HTTPStatus.UNAUTHORIZED)
259
260 def get_role_list(self):
261 """
262 Get role list.
263
264 :return: returns the list of roles.
265 """
266 try:
267 role_list = self.db.get_list("roles")
268 roles = [{"name": role["name"], "_id": role["_id"]} for role in role_list] # if role.name != "service" ?
269 return roles
270 except Exception:
271 raise AuthException("Error during role listing using internal backend", http_code=HTTPStatus.UNAUTHORIZED)
272
273 def create_role(self, role):
274 """
275 Create a role.
276
277 :param role: role name.
278 :raises AuthconnOperationException: if role creation failed.
279 """
280 # try:
281 # TODO: Check that role name does not exist ?
282 return str(uuid4())
283 # except Exception:
284 # raise AuthconnOperationException("Error during role creation using internal backend")
285 # except Conflict as ex:
286 # self.logger.info("Duplicate entry: %s", str(ex))
287
288 def delete_role(self, role_id):
289 """
290 Delete a role.
291
292 :param role_id: role identifier.
293 :raises AuthconnOperationException: if role deletion failed.
294 """
295 # try:
296 # TODO: Check that role exists ?
297 return True
298 # except Exception:
299 # raise AuthconnOperationException("Error during role deletion using internal backend")