1 # -*- coding: utf-8 -*-
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
9 __author__
= "Eduardo Sousa <esousa@whitestack.com>"
10 __date__
= "$27-jul-2018 23:59:59$"
13 from base64
import standard_b64decode
14 from copy
import deepcopy
15 from functools
import reduce
16 from http
import HTTPStatus
21 from authconn
import AuthException
22 from authconn_keystone
import AuthconnKeystone
23 from engine
import EngineException
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.
34 def __init__(self
, engine
):
36 Authenticator initializer. Setup the initial state of the object,
37 while it waits for the config dictionary and database initialization.
39 Note: engine is only here until all the calls can to it can be replaced.
41 :param engine: reference to engine object used.
51 self
.logger
= logging
.getLogger("nbi.authenticator")
53 def start(self
, config
):
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.
59 :param config: dictionary containing the relevant parameters for this object.
65 if config
["authentication"]["backend"] == "keystone":
66 self
.backend
= AuthconnKeystone(self
.config
["authentication"])
67 elif config
["authentication"]["backend"] == "internal":
70 raise Exception("No authentication backend defined")
73 # TODO: Implement database initialization
74 # NOTE: Database needed to store the mappings
75 except Exception as e
:
76 raise AuthException(str(e
))
78 def init_db(self
, target_version
='1.0'):
80 Check if the database has been initialized. If not, create the required tables
81 and insert the predefined mappings between roles and permissions.
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.
92 # 1. Get token Authorization bearer
93 auth
= cherrypy
.request
.headers
.get("Authorization")
95 auth_list
= auth
.split(" ")
96 if auth_list
[0].lower() == "bearer":
98 elif auth_list
[0].lower() == "basic":
99 user_passwd64
= auth_list
[-1]
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
111 user_passwd
= standard_b64decode(user_passwd64
).decode()
112 user
, _
, passwd
= user_passwd
.partition(":")
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
)
122 self
.backend
.validate_token(token
)
123 return self
.tokens
[token
]
124 except AuthException
:
125 self
.del_token(token
)
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
))
133 def new_token(self
, session
, indata
, remote
):
134 if self
.config
["authentication"]["backend"] == "internal":
135 return self
.engine
.new_token(session
, indata
, remote
)
137 if indata
.get("username"):
138 token
, projects
= self
.backend
.authenticate_with_user_password(
139 indata
.get("username"), indata
.get("password"))
141 token
, projects
= self
.backend
.authenticate_with_token(
142 session
.get("id"), indata
.get("project_id"))
144 raise AuthException("Provide credentials: username/password or Authorization Bearer token",
145 http_code
=HTTPStatus
.UNAUTHORIZED
)
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
)
153 project_id
= projects
[0]
155 if project_id
== "admin":
158 session_admin
= reduce(lambda x
, y
: x
or (True if y
== "admin" else False),
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
174 new_session
["remote_host"] = remote
.name
176 new_session
["remote_host"] = remote
.ip
178 self
.tokens
[token
] = new_session
180 return deepcopy(new_session
)
182 def get_token_list(self
, session
):
183 if self
.config
["authentication"]["backend"] == "internal":
184 return self
.engine
.get_token_list(session
)
186 return [deepcopy(token
) for token
in self
.tokens
.values()
187 if token
["username"] == session
["username"]]
189 def get_token(self
, session
, token
):
190 if self
.config
["authentication"]["backend"] == "internal":
191 return self
.engine
.get_token(session
, token
)
193 token_value
= self
.tokens
.get(token
)
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
)
200 def del_token(self
, token
):
201 if self
.config
["authentication"]["backend"] == "internal":
202 return self
.engine
.del_token(token
)
205 self
.backend
.revoke_token(token
)
206 del self
.tokens
[token
]
207 return "token '{}' deleted".format(token
)
209 raise EngineException("Token '{}' not found".format(token
), http_code
=HTTPStatus
.NOT_FOUND
)