blob: 4ee9ce23ab2ee6bec66683d1f887537fa361de7c [file] [log] [blame]
Eduardo Sousa819d34c2018-07-31 01:20:02 +01001# -*- coding: utf-8 -*-
2
3"""
4Authenticator is responsible for authenticating the users,
5create the tokens unscoped and scoped, retrieve the role
6list 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
12import logging
Eduardo Sousa2f988212018-07-26 01:04:11 +010013from base64 import standard_b64decode
Eduardo Sousa819d34c2018-07-31 01:20:02 +010014from copy import deepcopy
15from functools import reduce
Eduardo Sousa2f988212018-07-26 01:04:11 +010016from http import HTTPStatus
Eduardo Sousa819d34c2018-07-31 01:20:02 +010017from time import time
Eduardo Sousa2f988212018-07-26 01:04:11 +010018
Eduardo Sousa819d34c2018-07-31 01:20:02 +010019import cherrypy
Eduardo Sousa2f988212018-07-26 01:04:11 +010020
Eduardo Sousa819d34c2018-07-31 01:20:02 +010021from authconn import AuthException
22from authconn_keystone import AuthconnKeystone
Eduardo Sousa2f988212018-07-26 01:04:11 +010023from engine import EngineException
24
Eduardo Sousa2f988212018-07-26 01:04:11 +010025
Eduardo Sousa819d34c2018-07-31 01:20:02 +010026class 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 """
Eduardo Sousa2f988212018-07-26 01:04:11 +010033
Eduardo Sousa2f988212018-07-26 01:04:11 +010034 def __init__(self, engine):
Eduardo Sousa819d34c2018-07-31 01:20:02 +010035 """
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 """
Eduardo Sousa2f988212018-07-26 01:04:11 +010043 super().__init__()
44
45 self.engine = engine
46
Eduardo Sousa819d34c2018-07-31 01:20:02 +010047 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
Eduardo Sousa2f988212018-07-26 01:04:11 +010088 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
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100115 outdata = self.new_token(None, {"username": user, "password": passwd})
Eduardo Sousa2f988212018-07-26 01:04:11 +0100116 token = outdata["id"]
117 cherrypy.session['Authorization'] = token
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100118 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
Eduardo Sousa2f988212018-07-26 01:04:11 +0100127 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)
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100131 raise AuthException(str(e))
Eduardo Sousa2f988212018-07-26 01:04:11 +0100132
133 def new_token(self, session, indata, remote):
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100134 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)
Eduardo Sousa2f988212018-07-26 01:04:11 +0100181
182 def get_token_list(self, session):
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100183 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"]]
Eduardo Sousa2f988212018-07-26 01:04:11 +0100188
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100189 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
Eduardo Sousa2f988212018-07-26 01:04:11 +0100199
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100200 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)