Adding Authentication Connector plugin system
[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 logging
13 from base64 import standard_b64decode
14 from copy import deepcopy
15 from functools import reduce
16 from http import HTTPStatus
17 from time import time
18
19 import cherrypy
20
21 from authconn import AuthException
22 from authconn_keystone import AuthconnKeystone
23 from engine import EngineException
24
25
26 class Authenticator:
27 """
28 This class should hold all the mechanisms for User Authentication and
29 Authorization. Initially it should support Openstack Keystone as a
30 backend through a plugin model where more backends can be added and a
31 RBAC model to manage permissions on operations.
32 """
33
34 def __init__(self, engine):
35 """
36 Authenticator initializer. Setup the initial state of the object,
37 while it waits for the config dictionary and database initialization.
38
39 Note: engine is only here until all the calls can to it can be replaced.
40
41 :param engine: reference to engine object used.
42 """
43 super().__init__()
44
45 self.engine = engine
46
47 self.backend = None
48 self.config = None
49 self.db = None
50 self.tokens = dict()
51 self.logger = logging.getLogger("nbi.authenticator")
52
53 def start(self, config):
54 """
55 Method to configure the Authenticator object. This method should be called
56 after object creation. It is responsible by initializing the selected backend,
57 as well as the initialization of the database connection.
58
59 :param config: dictionary containing the relevant parameters for this object.
60 """
61 self.config = config
62
63 try:
64 if not self.backend:
65 if config["authentication"]["backend"] == "keystone":
66 self.backend = AuthconnKeystone(self.config["authentication"])
67 elif config["authentication"]["backend"] == "internal":
68 pass
69 else:
70 raise Exception("No authentication backend defined")
71 if not self.db:
72 pass
73 # TODO: Implement database initialization
74 # NOTE: Database needed to store the mappings
75 except Exception as e:
76 raise AuthException(str(e))
77
78 def init_db(self, target_version='1.0'):
79 """
80 Check if the database has been initialized. If not, create the required tables
81 and insert the predefined mappings between roles and permissions.
82
83 :param target_version: schema version that should be present in the database.
84 :return: None if OK, exception if error or version is different.
85 """
86 pass
87
88 def authorize(self):
89 token = None
90 user_passwd64 = None
91 try:
92 # 1. Get token Authorization bearer
93 auth = cherrypy.request.headers.get("Authorization")
94 if auth:
95 auth_list = auth.split(" ")
96 if auth_list[0].lower() == "bearer":
97 token = auth_list[-1]
98 elif auth_list[0].lower() == "basic":
99 user_passwd64 = auth_list[-1]
100 if not token:
101 if cherrypy.session.get("Authorization"):
102 # 2. Try using session before request a new token. If not, basic authentication will generate
103 token = cherrypy.session.get("Authorization")
104 if token == "logout":
105 token = None # force Unauthorized response to insert user pasword again
106 elif user_passwd64 and cherrypy.request.config.get("auth.allow_basic_authentication"):
107 # 3. Get new token from user password
108 user = None
109 passwd = None
110 try:
111 user_passwd = standard_b64decode(user_passwd64).decode()
112 user, _, passwd = user_passwd.partition(":")
113 except Exception:
114 pass
115 outdata = self.new_token(None, {"username": user, "password": passwd})
116 token = outdata["id"]
117 cherrypy.session['Authorization'] = token
118 if self.config["authentication"]["backend"] == "internal":
119 return self.engine.authorize(token)
120 else:
121 try:
122 self.backend.validate_token(token)
123 return self.tokens[token]
124 except AuthException:
125 self.del_token(token)
126 raise
127 except EngineException as e:
128 if cherrypy.session.get('Authorization'):
129 del cherrypy.session['Authorization']
130 cherrypy.response.headers["WWW-Authenticate"] = 'Bearer realm="{}"'.format(e)
131 raise AuthException(str(e))
132
133 def new_token(self, session, indata, remote):
134 if self.config["authentication"]["backend"] == "internal":
135 return self.engine.new_token(session, indata, remote)
136 else:
137 if indata.get("username"):
138 token, projects = self.backend.authenticate_with_user_password(
139 indata.get("username"), indata.get("password"))
140 elif session:
141 token, projects = self.backend.authenticate_with_token(
142 session.get("id"), indata.get("project_id"))
143 else:
144 raise AuthException("Provide credentials: username/password or Authorization Bearer token",
145 http_code=HTTPStatus.UNAUTHORIZED)
146
147 if indata.get("project_id"):
148 project_id = indata.get("project_id")
149 if project_id not in projects:
150 raise AuthException("Project {} not allowed for this user".format(project_id),
151 http_code=HTTPStatus.UNAUTHORIZED)
152 else:
153 project_id = projects[0]
154
155 if project_id == "admin":
156 session_admin = True
157 else:
158 session_admin = reduce(lambda x, y: x or (True if y == "admin" else False),
159 projects, False)
160
161 now = time()
162 new_session = {
163 "_id": token,
164 "id": token,
165 "issued_at": now,
166 "expires": now+3600,
167 "project_id": project_id,
168 "username": indata.get("username") if not session else session.get("username"),
169 "remote_port": remote.port,
170 "admin": session_admin
171 }
172
173 if remote.name:
174 new_session["remote_host"] = remote.name
175 elif remote.ip:
176 new_session["remote_host"] = remote.ip
177
178 self.tokens[token] = new_session
179
180 return deepcopy(new_session)
181
182 def get_token_list(self, session):
183 if self.config["authentication"]["backend"] == "internal":
184 return self.engine.get_token_list(session)
185 else:
186 return [deepcopy(token) for token in self.tokens.values()
187 if token["username"] == session["username"]]
188
189 def get_token(self, session, token):
190 if self.config["authentication"]["backend"] == "internal":
191 return self.engine.get_token(session, token)
192 else:
193 token_value = self.tokens.get(token)
194 if not token_value:
195 raise EngineException("token not found", http_code=HTTPStatus.NOT_FOUND)
196 if token_value["username"] != session["username"] and not session["admin"]:
197 raise EngineException("needed admin privileges", http_code=HTTPStatus.UNAUTHORIZED)
198 return token_value
199
200 def del_token(self, token):
201 if self.config["authentication"]["backend"] == "internal":
202 return self.engine.del_token(token)
203 else:
204 try:
205 self.backend.revoke_token(token)
206 del self.tokens[token]
207 return "token '{}' deleted".format(token)
208 except KeyError:
209 raise EngineException("Token '{}' not found".format(token), http_code=HTTPStatus.NOT_FOUND)