Adding slice templates to NBI
[osm/NBI.git] / osm_nbi / auth.py
1 # -*- coding: utf-8 -*-
2
3 """
4 Authenticator is responsible for authenticating the users,
5 create the tokens unscoped and scoped, retrieve the role
6 list inside the projects that they are inserted
7 """
8
9 __author__ = "Eduardo Sousa <esousa@whitestack.com>"
10 __date__ = "$27-jul-2018 23:59:59$"
11
12 import cherrypy
13 import logging
14 from base64 import standard_b64decode
15 from copy import deepcopy
16 from functools import reduce
17 from hashlib import sha256
18 from http import HTTPStatus
19 from random import choice as random_choice
20 from time import time
21
22 from authconn import AuthException
23 from authconn_keystone import AuthconnKeystone
24 from osm_common import dbmongo
25 from osm_common import dbmemory
26 from osm_common.dbbase import DbException
27
28
29 class Authenticator:
30 """
31 This class should hold all the mechanisms for User Authentication and
32 Authorization. Initially it should support Openstack Keystone as a
33 backend through a plugin model where more backends can be added and a
34 RBAC model to manage permissions on operations.
35 """
36
37 def __init__(self):
38 """
39 Authenticator initializer. Setup the initial state of the object,
40 while it waits for the config dictionary and database initialization.
41 """
42 self.backend = None
43 self.config = None
44 self.db = None
45 self.tokens = dict()
46 self.logger = logging.getLogger("nbi.authenticator")
47
48 def start(self, config):
49 """
50 Method to configure the Authenticator object. This method should be called
51 after object creation. It is responsible by initializing the selected backend,
52 as well as the initialization of the database connection.
53
54 :param config: dictionary containing the relevant parameters for this object.
55 """
56 self.config = config
57
58 try:
59 if not self.backend:
60 if config["authentication"]["backend"] == "keystone":
61 self.backend = AuthconnKeystone(self.config["authentication"])
62 elif config["authentication"]["backend"] == "internal":
63 pass
64 else:
65 raise AuthException("Unknown authentication backend: {}"
66 .format(config["authentication"]["backend"]))
67 if not self.db:
68 if config["database"]["driver"] == "mongo":
69 self.db = dbmongo.DbMongo()
70 self.db.db_connect(config["database"])
71 elif config["database"]["driver"] == "memory":
72 self.db = dbmemory.DbMemory()
73 self.db.db_connect(config["database"])
74 else:
75 raise AuthException("Invalid configuration param '{}' at '[database]':'driver'"
76 .format(config["database"]["driver"]))
77 except Exception as e:
78 raise AuthException(str(e))
79
80 def stop(self):
81 try:
82 if self.db:
83 self.db.db_disconnect()
84 except DbException as e:
85 raise AuthException(str(e), http_code=e.http_code)
86
87 def init_db(self, target_version='1.1'):
88 """
89 Check if the database has been initialized, with at least one user. If not, create an adthe required tables
90 and insert the predefined mappings between roles and permissions.
91
92 :param target_version: schema version that should be present in the database.
93 :return: None if OK, exception if error or version is different.
94 """
95 pass
96
97 def authorize(self):
98 token = None
99 user_passwd64 = None
100 try:
101 # 1. Get token Authorization bearer
102 auth = cherrypy.request.headers.get("Authorization")
103 if auth:
104 auth_list = auth.split(" ")
105 if auth_list[0].lower() == "bearer":
106 token = auth_list[-1]
107 elif auth_list[0].lower() == "basic":
108 user_passwd64 = auth_list[-1]
109 if not token:
110 if cherrypy.session.get("Authorization"):
111 # 2. Try using session before request a new token. If not, basic authentication will generate
112 token = cherrypy.session.get("Authorization")
113 if token == "logout":
114 token = None # force Unauthorized response to insert user pasword again
115 elif user_passwd64 and cherrypy.request.config.get("auth.allow_basic_authentication"):
116 # 3. Get new token from user password
117 user = None
118 passwd = None
119 try:
120 user_passwd = standard_b64decode(user_passwd64).decode()
121 user, _, passwd = user_passwd.partition(":")
122 except Exception:
123 pass
124 outdata = self.new_token(None, {"username": user, "password": passwd})
125 token = outdata["id"]
126 cherrypy.session['Authorization'] = token
127 if self.config["authentication"]["backend"] == "internal":
128 return self._internal_authorize(token)
129 else:
130 try:
131 self.backend.validate_token(token)
132 return self.tokens[token]
133 except AuthException:
134 self.del_token(token)
135 raise
136 except AuthException as e:
137 if cherrypy.session.get('Authorization'):
138 del cherrypy.session['Authorization']
139 cherrypy.response.headers["WWW-Authenticate"] = 'Bearer realm="{}"'.format(e)
140 raise AuthException(str(e))
141
142 def new_token(self, session, indata, remote):
143 if self.config["authentication"]["backend"] == "internal":
144 return self._internal_new_token(session, indata, remote)
145 else:
146 if indata.get("username"):
147 token, projects = self.backend.authenticate_with_user_password(
148 indata.get("username"), indata.get("password"))
149 elif session:
150 token, projects = self.backend.authenticate_with_token(
151 session.get("id"), indata.get("project_id"))
152 else:
153 raise AuthException("Provide credentials: username/password or Authorization Bearer token",
154 http_code=HTTPStatus.UNAUTHORIZED)
155
156 if indata.get("project_id"):
157 project_id = indata.get("project_id")
158 if project_id not in projects:
159 raise AuthException("Project {} not allowed for this user".format(project_id),
160 http_code=HTTPStatus.UNAUTHORIZED)
161 else:
162 project_id = projects[0]
163
164 if project_id == "admin":
165 session_admin = True
166 else:
167 session_admin = reduce(lambda x, y: x or (True if y == "admin" else False),
168 projects, False)
169
170 now = time()
171 new_session = {
172 "_id": token,
173 "id": token,
174 "issued_at": now,
175 "expires": now + 3600,
176 "project_id": project_id,
177 "username": indata.get("username") if not session else session.get("username"),
178 "remote_port": remote.port,
179 "admin": session_admin
180 }
181
182 if remote.name:
183 new_session["remote_host"] = remote.name
184 elif remote.ip:
185 new_session["remote_host"] = remote.ip
186
187 self.tokens[token] = new_session
188
189 return deepcopy(new_session)
190
191 def get_token_list(self, session):
192 if self.config["authentication"]["backend"] == "internal":
193 return self._internal_get_token_list(session)
194 else:
195 return [deepcopy(token) for token in self.tokens.values()
196 if token["username"] == session["username"]]
197
198 def get_token(self, session, token):
199 if self.config["authentication"]["backend"] == "internal":
200 return self._internal_get_token(session, token)
201 else:
202 token_value = self.tokens.get(token)
203 if not token_value:
204 raise AuthException("token not found", http_code=HTTPStatus.NOT_FOUND)
205 if token_value["username"] != session["username"] and not session["admin"]:
206 raise AuthException("needed admin privileges", http_code=HTTPStatus.UNAUTHORIZED)
207 return token_value
208
209 def del_token(self, token):
210 if self.config["authentication"]["backend"] == "internal":
211 return self._internal_del_token(token)
212 else:
213 try:
214 self.backend.revoke_token(token)
215 del self.tokens[token]
216 return "token '{}' deleted".format(token)
217 except KeyError:
218 raise AuthException("Token '{}' not found".format(token), http_code=HTTPStatus.NOT_FOUND)
219
220 def _internal_authorize(self, token):
221 try:
222 if not token:
223 raise AuthException("Needed a token or Authorization http header", http_code=HTTPStatus.UNAUTHORIZED)
224 if token not in self.tokens:
225 raise AuthException("Invalid token or Authorization http header", http_code=HTTPStatus.UNAUTHORIZED)
226 session = self.tokens[token]
227 now = time()
228 if session["expires"] < now:
229 del self.tokens[token]
230 raise AuthException("Expired Token or Authorization http header", http_code=HTTPStatus.UNAUTHORIZED)
231 return session
232 except AuthException:
233 if self.config["global"].get("test.user_not_authorized"):
234 return {"id": "fake-token-id-for-test",
235 "project_id": self.config["global"].get("test.project_not_authorized", "admin"),
236 "username": self.config["global"]["test.user_not_authorized"]}
237 else:
238 raise
239
240 def _internal_new_token(self, session, indata, remote):
241 now = time()
242 user_content = None
243
244 # Try using username/password
245 if indata.get("username"):
246 user_rows = self.db.get_list("users", {"username": indata.get("username")})
247 user_content = None
248 if user_rows:
249 user_content = user_rows[0]
250 salt = user_content["_admin"]["salt"]
251 shadow_password = sha256(indata.get("password", "").encode('utf-8') + salt.encode('utf-8')).hexdigest()
252 if shadow_password != user_content["password"]:
253 user_content = None
254 if not user_content:
255 raise AuthException("Invalid username/password", http_code=HTTPStatus.UNAUTHORIZED)
256 elif session:
257 user_rows = self.db.get_list("users", {"username": session["username"]})
258 if user_rows:
259 user_content = user_rows[0]
260 else:
261 raise AuthException("Invalid token", http_code=HTTPStatus.UNAUTHORIZED)
262 else:
263 raise AuthException("Provide credentials: username/password or Authorization Bearer token",
264 http_code=HTTPStatus.UNAUTHORIZED)
265
266 token_id = ''.join(random_choice('abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789')
267 for _ in range(0, 32))
268 if indata.get("project_id"):
269 project_id = indata.get("project_id")
270 if project_id not in user_content["projects"]:
271 raise AuthException("project {} not allowed for this user"
272 .format(project_id), http_code=HTTPStatus.UNAUTHORIZED)
273 else:
274 project_id = user_content["projects"][0]
275 if project_id == "admin":
276 session_admin = True
277 else:
278 project = self.db.get_one("projects", {"_id": project_id})
279 session_admin = project.get("admin", False)
280 new_session = {"issued_at": now, "expires": now + 3600,
281 "_id": token_id, "id": token_id, "project_id": project_id, "username": user_content["username"],
282 "remote_port": remote.port, "admin": session_admin}
283 if remote.name:
284 new_session["remote_host"] = remote.name
285 elif remote.ip:
286 new_session["remote_host"] = remote.ip
287
288 self.tokens[token_id] = new_session
289 return deepcopy(new_session)
290
291 def _internal_get_token_list(self, session):
292 token_list = []
293 for token_id, token_value in self.tokens.items():
294 if token_value["username"] == session["username"]:
295 token_list.append(deepcopy(token_value))
296 return token_list
297
298 def _internal_get_token(self, session, token_id):
299 token_value = self.tokens.get(token_id)
300 if not token_value:
301 raise AuthException("token not found", http_code=HTTPStatus.NOT_FOUND)
302 if token_value["username"] != session["username"] and not session["admin"]:
303 raise AuthException("needed admin privileges", http_code=HTTPStatus.UNAUTHORIZED)
304 return token_value
305
306 def _internal_del_token(self, token_id):
307 try:
308 del self.tokens[token_id]
309 return "token '{}' deleted".format(token_id)
310 except KeyError:
311 raise AuthException("Token '{}' not found".format(token_id), http_code=HTTPStatus.NOT_FOUND)