d3258fe591811dd7c6cba494a399441767921da7
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 token_info
= self
.token_cache
.get(token
)
124 if token_info
and token_info
["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 token_info
= self
.db
.get_one("tokens", {"_id": token
})
132 if token_info
["expires"] < now
:
133 raise AuthException("Expired Token or Authorization HTTP header", http_code
=HTTPStatus
.UNAUTHORIZED
)
137 except DbException
as e
:
138 if e
.http_code
== HTTPStatus
.NOT_FOUND
:
139 raise AuthException("Invalid Token or Authorization HTTP header", http_code
=HTTPStatus
.UNAUTHORIZED
)
142 except AuthException
:
143 if self
.config
["global"].get("test.user_not_authorized"):
144 return {"id": "fake-token-id-for-test",
145 "project_id": self
.config
["global"].get("test.project_not_authorized", "admin"),
146 "username": self
.config
["global"]["test.user_not_authorized"], "admin": True}
150 self
.logger
.exception("Error during token validation using internal backend")
151 raise AuthException("Error during token validation using internal backend",
152 http_code
=HTTPStatus
.UNAUTHORIZED
)
154 def revoke_token(self
, token
):
158 :param token: token to be revoked
161 self
.token_cache
.pop(token
, None)
162 self
.db
.del_one("tokens", {"_id": token
})
164 except DbException
as e
:
165 if e
.http_code
== HTTPStatus
.NOT_FOUND
:
166 raise AuthException("Token '{}' not found".format(token
), http_code
=HTTPStatus
.NOT_FOUND
)
169 msg
= "Error during token revocation using internal backend"
170 self
.logger
.exception(msg
)
171 raise AuthException(msg
, http_code
=HTTPStatus
.UNAUTHORIZED
)
173 def authenticate(self
, user
, password
, project
=None, token_info
=None):
175 Authenticate a user using username/password or previous token_info plus project; its creates a new token
177 :param user: user: name, id or None
178 :param password: password or None
179 :param project: name, id, or None. If None first found project will be used to get an scope token
180 :param token_info: previous token_info to obtain authorization
181 :param remote: remote host information
182 :return: the scoped token info or raises an exception. The token is a dictionary with:
183 _id: token string id,
185 project_id: scoped_token project_id,
186 project_name: scoped_token project_name,
187 expires: epoch time when it expires,
194 # Try using username/password
196 user_rows
= self
.db
.get_list("users", {BaseTopic
.id_field("users", user
): user
})
198 user_content
= user_rows
[0]
199 salt
= user_content
["_admin"]["salt"]
200 shadow_password
= sha256(password
.encode('utf-8') + salt
.encode('utf-8')).hexdigest()
201 if shadow_password
!= user_content
["password"]:
204 raise AuthException("Invalid username/password", http_code
=HTTPStatus
.UNAUTHORIZED
)
206 user_rows
= self
.db
.get_list("users", {"username": token_info
["username"]})
208 user_content
= user_rows
[0]
210 raise AuthException("Invalid token", http_code
=HTTPStatus
.UNAUTHORIZED
)
212 raise AuthException("Provide credentials: username/password or Authorization Bearer token",
213 http_code
=HTTPStatus
.UNAUTHORIZED
)
215 token_id
= ''.join(random_choice('abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789')
216 for _
in range(0, 32))
218 # TODO when user contained project_role_mappings with project_id,project_ name this checking to
219 # database will not be needed
221 project
= user_content
["projects"][0]
223 # To allow project names in project_id
224 proj
= self
.db
.get_one("projects", {BaseTopic
.id_field("projects", project
): project
})
225 if proj
["_id"] not in user_content
["projects"] and proj
["name"] not in user_content
["projects"]:
226 raise AuthException("project {} not allowed for this user".format(project
),
227 http_code
=HTTPStatus
.UNAUTHORIZED
)
229 # TODO remove admin, this vill be used by roles RBAC
230 if proj
["name"] == "admin":
233 token_admin
= proj
.get("admin", False)
235 # TODO add token roles - PROVISIONAL. Get this list from user_content["project_role_mappings"]
236 role_id
= self
.db
.get_one("roles", {"name": "system_admin"})["_id"]
237 roles_list
= [{"name": "system_admin", "id": role_id
}]
239 new_token
= {"issued_at": now
,
240 "expires": now
+ 3600,
243 "project_id": proj
["_id"],
244 "project_name": proj
["name"],
245 "username": user_content
["username"],
246 "user_id": user_content
["_id"],
247 "admin": token_admin
,
251 self
.token_cache
[token_id
] = new_token
252 self
.db
.create("tokens", new_token
)
253 return deepcopy(new_token
)
255 except Exception as e
:
256 msg
= "Error during user authentication using internal backend: {}".format(e
)
257 self
.logger
.exception(msg
)
258 raise AuthException(msg
, http_code
=HTTPStatus
.UNAUTHORIZED
)
260 def get_role_list(self
):
264 :return: returns the list of roles.
267 role_list
= self
.db
.get_list("roles")
268 roles
= [{"name": role
["name"], "_id": role
["_id"]} for role
in role_list
] # if role.name != "service" ?
271 raise AuthException("Error during role listing using internal backend", http_code
=HTTPStatus
.UNAUTHORIZED
)
273 def create_role(self
, role
):
277 :param role: role name.
278 :raises AuthconnOperationException: if role creation failed.
281 # TODO: Check that role name does not exist ?
284 # raise AuthconnOperationException("Error during role creation using internal backend")
285 # except Conflict as ex:
286 # self.logger.info("Duplicate entry: %s", str(ex))
288 def delete_role(self
, role_id
):
292 :param role_id: role identifier.
293 :raises AuthconnOperationException: if role deletion failed.
296 # TODO: Check that role exists ?
299 # raise AuthconnOperationException("Error during role deletion using internal backend")