1 # -*- coding: utf-8 -*-
3 # Copyright 2018 Telefonica S.A.
4 # Copyright 2018 ALTRAN InnovaciĆ³n S.L.
6 # Licensed under the Apache License, Version 2.0 (the "License"); you may
7 # not use this file except in compliance with the License. You may obtain
8 # a copy of the License at
10 # http://www.apache.org/licenses/LICENSE-2.0
12 # Unless required by applicable law or agreed to in writing, software
13 # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
14 # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
15 # License for the specific language governing permissions and limitations
18 # For those usages not covered by the Apache License, Version 2.0 please
19 # contact: esousa@whitestack.com or glavado@whitestack.com
23 AuthconnInternal implements implements the connector for
24 OSM Internal Authentication Backend and leverages the RBAC model
27 __author__
= "Pedro de la Cruz Ramos <pdelacruzramos@altran.com>"
28 __date__
= "$06-jun-2019 11:16:08$"
30 from authconn
import Authconn
, AuthException
31 from osm_common
.dbbase
import DbException
32 from base_topic
import BaseTopic
36 from http
import HTTPStatus
37 from uuid
import uuid4
38 from hashlib
import sha256
39 from copy
import deepcopy
40 from random
import choice
as random_choice
43 class AuthconnInternal(Authconn
):
44 def __init__(self
, config
, db
, token_cache
):
45 Authconn
.__init
__(self
, config
)
47 self
.logger
= logging
.getLogger("nbi.authenticator.internal")
50 # self.xxx = config.get("xxx", "default")
53 self
.token_cache
= token_cache
59 # def create_token (self, user, password, projects=[], project=None, remote=None):
62 # def authenticate_with_user_password(self, user, password, project=None, remote=None):
65 # def authenticate_with_token(self, token, project=None, remote=None):
68 # def get_user_project_list(self, token):
71 # def get_user_role_list(self, token):
74 # def create_user(self, user, password):
77 # def change_password(self, user, new_password):
80 # def delete_user(self, user_id):
83 # def get_user_list(self, filter_q={}):
86 # def get_project_list(self, filter_q={}):
89 # def create_project(self, project):
92 # def delete_project(self, project_id):
95 # def assign_role_to_user(self, user, project, role):
96 # Not required in Phase 1
98 # def remove_role_from_user(self, user, project, role):
99 # Not required in Phase 1
101 def validate_token(self
, token
):
103 Check if the token is valid.
105 :param token: token to validate
106 :return: dictionary with information associated with the token:
108 "project_id": project id
109 "project_name": project name
111 "username": user name
112 "roles": list with dict containing {name, id}
113 "expires": expiration date
114 If the token is not valid an exception is raised.
119 raise AuthException("Needed a token or Authorization HTTP header", http_code
=HTTPStatus
.UNAUTHORIZED
)
121 # try to get from cache first
123 session
= self
.token_cache
.get(token
)
124 if session
and session
["expires"] < now
:
125 # delete token. MUST be done with care, as another thread maybe already delete it. Do not use del
126 self
.token_cache
.pop(token
, None)
129 # get from database if not in cache
131 session
= self
.db
.get_one("tokens", {"_id": token
})
132 if session
["expires"] < now
:
133 raise AuthException("Expired Token or Authorization HTTP header", http_code
=HTTPStatus
.UNAUTHORIZED
)
135 # complete token information
136 pid
= session
["project_id"]
137 prj
= self
.db
.get_one("projects", {BaseTopic
.id_field("projects", pid
): pid
})
138 session
["project_id"] = prj
["_id"]
139 session
["project_name"] = prj
["name"]
140 session
["user_id"] = self
.db
.get_one("users", {"username": session
["username"]})["_id"]
142 # add token roles - PROVISIONAL
143 role_id
= self
.db
.get_one("roles", {"name": "system_admin"})["_id"]
144 session
["roles"] = [{"name": "system_admin", "id": role_id
}]
148 except DbException
as e
:
149 if e
.http_code
== HTTPStatus
.NOT_FOUND
:
150 raise AuthException("Invalid Token or Authorization HTTP header", http_code
=HTTPStatus
.UNAUTHORIZED
)
153 except AuthException
:
154 if self
.config
["global"].get("test.user_not_authorized"):
155 return {"id": "fake-token-id-for-test",
156 "project_id": self
.config
["global"].get("test.project_not_authorized", "admin"),
157 "username": self
.config
["global"]["test.user_not_authorized"], "admin": True}
161 self
.logger
.exception("Error during token validation using internal backend")
162 raise AuthException("Error during token validation using internal backend",
163 http_code
=HTTPStatus
.UNAUTHORIZED
)
165 def revoke_token(self
, token
):
169 :param token: token to be revoked
172 self
.token_cache
.pop(token
, None)
173 self
.db
.del_one("tokens", {"_id": token
})
175 except DbException
as e
:
176 if e
.http_code
== HTTPStatus
.NOT_FOUND
:
177 raise AuthException("Token '{}' not found".format(token
), http_code
=HTTPStatus
.NOT_FOUND
)
180 msg
= "Error during token revocation using internal backend"
181 self
.logger
.exception(msg
)
182 raise AuthException(msg
, http_code
=HTTPStatus
.UNAUTHORIZED
)
184 def authenticate(self
, user
, password
, project
=None, token
=None):
186 Authenticate a user using username/password or token, plus project
188 :param user: user: name, id or None
189 :param password: password or None
190 :param project: name, id, or None. If None first found project will be used to get an scope token
191 :param token: previous token to obtain authorization
192 :param remote: remote host information
193 :return: the scoped token info or raises an exception. The token is a dictionary with:
194 _id: token string id,
196 project_id: scoped_token project_id,
197 project_name: scoped_token project_name,
198 expires: epoch time when it expires,
205 # Try using username/password
207 user_rows
= self
.db
.get_list("users", {BaseTopic
.id_field("users", user
): user
})
209 user_content
= user_rows
[0]
210 salt
= user_content
["_admin"]["salt"]
211 shadow_password
= sha256(password
.encode('utf-8') + salt
.encode('utf-8')).hexdigest()
212 if shadow_password
!= user_content
["password"]:
215 raise AuthException("Invalid username/password", http_code
=HTTPStatus
.UNAUTHORIZED
)
217 user_rows
= self
.db
.get_list("users", {"username": token
["username"]})
219 user_content
= user_rows
[0]
221 raise AuthException("Invalid token", http_code
=HTTPStatus
.UNAUTHORIZED
)
223 raise AuthException("Provide credentials: username/password or Authorization Bearer token",
224 http_code
=HTTPStatus
.UNAUTHORIZED
)
226 token_id
= ''.join(random_choice('abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789')
227 for _
in range(0, 32))
231 if project_id
!= "admin":
232 # To allow project names in project_id
233 proj
= self
.db
.get_one("projects", {BaseTopic
.id_field("projects", project_id
): project_id
})
234 if proj
["_id"] not in user_content
["projects"] and proj
["name"] not in user_content
["projects"]:
235 raise AuthException("project {} not allowed for this user"
236 .format(project_id
), http_code
=HTTPStatus
.UNAUTHORIZED
)
238 project_id
= user_content
["projects"][0]
240 if project_id
== "admin":
243 # To allow project names in project_id
244 proj
= self
.db
.get_one("projects", {BaseTopic
.id_field("projects", project_id
): project_id
})
245 token_admin
= proj
.get("admin", False)
247 new_token
= {"issued_at": now
, "expires": now
+ 3600,
248 "_id": token_id
, "id": token_id
,
249 "project_id": project_id
,
250 "username": user_content
["username"],
251 "admin": token_admin
}
253 self
.token_cache
[token_id
] = new_token
254 self
.db
.create("tokens", new_token
)
255 # self._internal_tokens_prune(now) # Belongs to Authenticator - REMOVE?
256 return deepcopy(new_token
)
258 except Exception as e
:
259 msg
= "Error during user authentication using internal backend: {}".format(e
)
260 self
.logger
.exception(msg
)
261 raise AuthException(msg
, http_code
=HTTPStatus
.UNAUTHORIZED
)
263 def get_role_list(self
):
267 :return: returns the list of roles.
270 role_list
= self
.db
.get_list("roles")
271 roles
= [{"name": role
["name"], "_id": role
["_id"]} for role
in role_list
] # if role.name != "service" ?
274 raise AuthException("Error during role listing using internal backend", http_code
=HTTPStatus
.UNAUTHORIZED
)
276 def create_role(self
, role
):
280 :param role: role name.
281 :raises AuthconnOperationException: if role creation failed.
284 # TODO: Check that role name does not exist ?
287 # raise AuthconnOperationException("Error during role creation using internal backend")
288 # except Conflict as ex:
289 # self.logger.info("Duplicate entry: %s", str(ex))
291 def delete_role(self
, role_id
):
295 :param role_id: role identifier.
296 :raises AuthconnOperationException: if role deletion failed.
299 # TODO: Check that role exists ?
302 # raise AuthconnOperationException("Error during role deletion using internal backend")