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$"
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
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
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.
39 Authenticator initializer. Setup the initial state of the object,
40 while it waits for the config dictionary and database initialization.
46 self
.logger
= logging
.getLogger("nbi.authenticator")
48 def start(self
, config
):
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.
54 :param config: dictionary containing the relevant parameters for this object.
60 if config
["authentication"]["backend"] == "keystone":
61 self
.backend
= AuthconnKeystone(self
.config
["authentication"])
62 elif config
["authentication"]["backend"] == "internal":
65 raise AuthException("Unknown authentication backend: {}"
66 .format(config
["authentication"]["backend"]))
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"])
75 raise AuthException("Invalid configuration param '{}' at '[database]':'driver'"
76 .format(config
["database"]["driver"]))
77 except Exception as e
:
78 raise AuthException(str(e
))
83 self
.db
.db_disconnect()
84 except DbException
as e
:
85 raise AuthException(str(e
), http_code
=e
.http_code
)
87 def init_db(self
, target_version
='1.1'):
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.
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.
101 # 1. Get token Authorization bearer
102 auth
= cherrypy
.request
.headers
.get("Authorization")
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]
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
120 user_passwd
= standard_b64decode(user_passwd64
).decode()
121 user
, _
, passwd
= user_passwd
.partition(":")
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
)
131 self
.backend
.validate_token(token
)
132 return self
.tokens
[token
]
133 except AuthException
:
134 self
.del_token(token
)
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
))
142 def new_token(self
, session
, indata
, remote
):
143 if self
.config
["authentication"]["backend"] == "internal":
144 return self
._internal
_new
_token
(session
, indata
, remote
)
146 if indata
.get("username"):
147 token
, projects
= self
.backend
.authenticate_with_user_password(
148 indata
.get("username"), indata
.get("password"))
150 token
, projects
= self
.backend
.authenticate_with_token(
151 session
.get("id"), indata
.get("project_id"))
153 raise AuthException("Provide credentials: username/password or Authorization Bearer token",
154 http_code
=HTTPStatus
.UNAUTHORIZED
)
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
)
162 project_id
= projects
[0]
164 if project_id
== "admin":
167 session_admin
= reduce(lambda x
, y
: x
or (True if y
== "admin" else False),
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
183 new_session
["remote_host"] = remote
.name
185 new_session
["remote_host"] = remote
.ip
187 self
.tokens
[token
] = new_session
189 return deepcopy(new_session
)
191 def get_token_list(self
, session
):
192 if self
.config
["authentication"]["backend"] == "internal":
193 return self
._internal
_get
_token
_list
(session
)
195 return [deepcopy(token
) for token
in self
.tokens
.values()
196 if token
["username"] == session
["username"]]
198 def get_token(self
, session
, token
):
199 if self
.config
["authentication"]["backend"] == "internal":
200 return self
._internal
_get
_token
(session
, token
)
202 token_value
= self
.tokens
.get(token
)
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
)
209 def del_token(self
, token
):
210 if self
.config
["authentication"]["backend"] == "internal":
211 return self
._internal
_del
_token
(token
)
214 self
.backend
.revoke_token(token
)
215 del self
.tokens
[token
]
216 return "token '{}' deleted".format(token
)
218 raise AuthException("Token '{}' not found".format(token
), http_code
=HTTPStatus
.NOT_FOUND
)
220 def _internal_authorize(self
, 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
]
228 if session
["expires"] < now
:
229 del self
.tokens
[token
]
230 raise AuthException("Expired Token or Authorization http header", http_code
=HTTPStatus
.UNAUTHORIZED
)
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"]}
240 def _internal_new_token(self
, session
, indata
, remote
):
244 # Try using username/password
245 if indata
.get("username"):
246 user_rows
= self
.db
.get_list("users", {"username": indata
.get("username")})
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"]:
255 raise AuthException("Invalid username/password", http_code
=HTTPStatus
.UNAUTHORIZED
)
257 user_rows
= self
.db
.get_list("users", {"username": session
["username"]})
259 user_content
= user_rows
[0]
261 raise AuthException("Invalid token", http_code
=HTTPStatus
.UNAUTHORIZED
)
263 raise AuthException("Provide credentials: username/password or Authorization Bearer token",
264 http_code
=HTTPStatus
.UNAUTHORIZED
)
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
)
274 project_id
= user_content
["projects"][0]
275 if project_id
== "admin":
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
}
284 new_session
["remote_host"] = remote
.name
286 new_session
["remote_host"] = remote
.ip
288 self
.tokens
[token_id
] = new_session
289 return deepcopy(new_session
)
291 def _internal_get_token_list(self
, session
):
293 for token_id
, token_value
in self
.tokens
.items():
294 if token_value
["username"] == session
["username"]:
295 token_list
.append(deepcopy(token_value
))
298 def _internal_get_token(self
, session
, token_id
):
299 token_value
= self
.tokens
.get(token_id
)
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
)
306 def _internal_del_token(self
, token_id
):
308 del self
.tokens
[token_id
]
309 return "token '{}' deleted".format(token_id
)
311 raise AuthException("Token '{}' not found".format(token_id
), http_code
=HTTPStatus
.NOT_FOUND
)