40d32152f0f5ada08b54b272e42d6691ed74df58
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
# , AuthconnOperationException
31 from osm_common
.dbbase
import DbException
32 from base_topic
import BaseTopic
37 from http
import HTTPStatus
38 from uuid
import uuid4
39 from hashlib
import sha256
40 from copy
import deepcopy
41 from random
import choice
as random_choice
44 class AuthconnInternal(Authconn
):
45 def __init__(self
, config
, db
, token_cache
):
46 Authconn
.__init
__(self
, config
, db
, token_cache
)
48 self
.logger
= logging
.getLogger("nbi.authenticator.internal")
51 # self.xxx = config.get("xxx", "default")
54 self
.token_cache
= token_cache
60 def validate_token(self
, token
):
62 Check if the token is valid.
64 :param token: token to validate
65 :return: dictionary with information associated with the token:
67 "project_id": project id
68 "project_name": project name
71 "roles": list with dict containing {name, id}
72 "expires": expiration date
73 If the token is not valid an exception is raised.
78 raise AuthException("Needed a token or Authorization HTTP header", http_code
=HTTPStatus
.UNAUTHORIZED
)
80 # try to get from cache first
82 token_info
= self
.token_cache
.get(token
)
83 if token_info
and token_info
["expires"] < now
:
84 # delete token. MUST be done with care, as another thread maybe already delete it. Do not use del
85 self
.token_cache
.pop(token
, None)
88 # get from database if not in cache
90 token_info
= self
.db
.get_one("tokens", {"_id": token
})
91 if token_info
["expires"] < now
:
92 raise AuthException("Expired Token or Authorization HTTP header", http_code
=HTTPStatus
.UNAUTHORIZED
)
96 except DbException
as e
:
97 if e
.http_code
== HTTPStatus
.NOT_FOUND
:
98 raise AuthException("Invalid Token or Authorization HTTP header", http_code
=HTTPStatus
.UNAUTHORIZED
)
101 except AuthException
:
102 if self
.config
["global"].get("test.user_not_authorized"):
103 return {"id": "fake-token-id-for-test",
104 "project_id": self
.config
["global"].get("test.project_not_authorized", "admin"),
105 "username": self
.config
["global"]["test.user_not_authorized"], "admin": True}
109 self
.logger
.exception("Error during token validation using internal backend")
110 raise AuthException("Error during token validation using internal backend",
111 http_code
=HTTPStatus
.UNAUTHORIZED
)
113 def revoke_token(self
, token
):
117 :param token: token to be revoked
120 self
.token_cache
.pop(token
, None)
121 self
.db
.del_one("tokens", {"_id": token
})
123 except DbException
as e
:
124 if e
.http_code
== HTTPStatus
.NOT_FOUND
:
125 raise AuthException("Token '{}' not found".format(token
), http_code
=HTTPStatus
.NOT_FOUND
)
128 msg
= "Error during token revocation using internal backend"
129 self
.logger
.exception(msg
)
130 raise AuthException(msg
, http_code
=HTTPStatus
.UNAUTHORIZED
)
132 def authenticate(self
, user
, password
, project
=None, token_info
=None):
134 Authenticate a user using username/password or previous token_info plus project; its creates a new token
136 :param user: user: name, id or None
137 :param password: password or None
138 :param project: name, id, or None. If None first found project will be used to get an scope token
139 :param token_info: previous token_info to obtain authorization
140 :param remote: remote host information
141 :return: the scoped token info or raises an exception. The token is a dictionary with:
142 _id: token string id,
144 project_id: scoped_token project_id,
145 project_name: scoped_token project_name,
146 expires: epoch time when it expires,
152 # Try using username/password
154 user_rows
= self
.db
.get_list("users", {BaseTopic
.id_field("users", user
): user
})
156 user_content
= user_rows
[0]
157 salt
= user_content
["_admin"]["salt"]
158 shadow_password
= sha256(password
.encode('utf-8') + salt
.encode('utf-8')).hexdigest()
159 if shadow_password
!= user_content
["password"]:
162 raise AuthException("Invalid username/password", http_code
=HTTPStatus
.UNAUTHORIZED
)
164 user_rows
= self
.db
.get_list("users", {"username": token_info
["username"]})
166 user_content
= user_rows
[0]
168 raise AuthException("Invalid token", http_code
=HTTPStatus
.UNAUTHORIZED
)
170 raise AuthException("Provide credentials: username/password or Authorization Bearer token",
171 http_code
=HTTPStatus
.UNAUTHORIZED
)
173 token_id
= ''.join(random_choice('abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789')
174 for _
in range(0, 32))
176 # projects = user_content.get("projects", [])
177 prm_list
= user_content
.get("project_role_mappings", [])
180 project
= prm_list
[0]["project"] if prm_list
else None
182 raise AuthException("can't find a default project for this user", http_code
=HTTPStatus
.UNAUTHORIZED
)
184 projects
= [prm
["project"] for prm
in prm_list
]
186 proj
= self
.db
.get_one("projects", {BaseTopic
.id_field("projects", project
): project
})
187 project_name
= proj
["name"]
188 project_id
= proj
["_id"]
189 if project_name
not in projects
and project_id
not in projects
:
190 raise AuthException("project {} not allowed for this user".format(project
),
191 http_code
=HTTPStatus
.UNAUTHORIZED
)
193 # TODO remove admin, this vill be used by roles RBAC
194 if project_name
== "admin":
197 token_admin
= proj
.get("admin", False)
203 if prm
["project"] in [project_id
, project_name
]:
204 role
= self
.db
.get_one("roles", {BaseTopic
.id_field("roles", prm
["role"]): prm
["role"]})
209 roles_list
.append({"name": rnm
, "id": rid
})
211 rid
= self
.db
.get_one("roles", {"name": "project_admin"})["_id"]
212 roles_list
= [{"name": "project_admin", "id": rid
}]
214 new_token
= {"issued_at": now
,
215 "expires": now
+ 3600,
218 "project_id": proj
["_id"],
219 "project_name": proj
["name"],
220 "username": user_content
["username"],
221 "user_id": user_content
["_id"],
222 "admin": token_admin
,
226 self
.token_cache
[token_id
] = new_token
227 self
.db
.create("tokens", new_token
)
228 return deepcopy(new_token
)
230 def get_role_list(self
, filter_q
={}):
234 :return: returns the list of roles.
236 return self
.db
.get_list("roles", filter_q
)
238 def create_role(self
, role_info
):
242 :param role_info: full role info.
243 :return: returns the role id.
244 :raises AuthconnOperationException: if role creation failed.
246 # TODO: Check that role name does not exist ?
248 role_info
["_id"] = rid
249 rid
= self
.db
.create("roles", role_info
)
252 def delete_role(self
, role_id
):
256 :param role_id: role identifier.
257 :raises AuthconnOperationException: if role deletion failed.
259 return self
.db
.del_one("roles", {"_id": role_id
})
261 def update_role(self
, role_info
):
265 :param role_info: full role info.
266 :return: returns the role name and id.
267 :raises AuthconnOperationException: if user creation failed.
269 rid
= role_info
["_id"]
270 self
.db
.set_one("roles", {"_id": rid
}, role_info
) # CONFIRM
271 return {"_id": rid
, "name": role_info
["name"]}
273 def create_user(self
, user_info
):
277 :param user_info: full user info.
278 :return: returns the username and id of the user.
280 BaseTopic
.format_on_new(user_info
, make_public
=False)
282 user_info
["_admin"]["salt"] = salt
283 if "password" in user_info
:
284 user_info
["password"] = sha256(user_info
["password"].encode('utf-8') + salt
.encode('utf-8')).hexdigest()
285 # "projects" are not stored any more
286 if "projects" in user_info
:
287 del user_info
["projects"]
288 self
.db
.create("users", user_info
)
289 return {"username": user_info
["username"], "_id": user_info
["_id"]}
291 def update_user(self
, user_info
):
293 Change the user name and/or password.
295 :param user_info: user info modifications
297 uid
= user_info
["_id"]
298 user_data
= self
.db
.get_one("users", {BaseTopic
.id_field("users", uid
): uid
})
299 BaseTopic
.format_on_edit(user_data
, user_info
)
301 usnm
= user_info
.get("username")
303 user_data
["username"] = usnm
304 # If password is given and is not already encripted
305 pswd
= user_info
.get("password")
306 if pswd
and (len(pswd
) != 64 or not re
.match('[a-fA-F0-9]*', pswd
)): # TODO: Improve check?
308 if "_admin" not in user_data
:
309 user_data
["_admin"] = {}
310 user_data
["_admin"]["salt"] = salt
311 user_data
["password"] = sha256(pswd
.encode('utf-8') + salt
.encode('utf-8')).hexdigest()
312 # Project-Role Mappings
313 # TODO: Check that user_info NEVER includes "project_role_mappings"
314 if "project_role_mappings" not in user_data
:
315 user_data
["project_role_mappings"] = []
316 for prm
in user_info
.get("add_project_role_mappings", []):
317 user_data
["project_role_mappings"].append(prm
)
318 for prm
in user_info
.get("remove_project_role_mappings", []):
319 for pidf
in ["project", "project_name"]:
320 for ridf
in ["role", "role_name"]:
322 user_data
["project_role_mappings"].remove({"role": prm
[ridf
], "project": prm
[pidf
]})
327 self
.db
.set_one("users", {BaseTopic
.id_field("users", uid
): uid
}, user_data
) # CONFIRM
329 def delete_user(self
, user_id
):
333 :param user_id: user identifier.
334 :raises AuthconnOperationException: if user deletion failed.
336 self
.db
.del_one("users", {"_id": user_id
})
339 def get_user_list(self
, filter_q
=None):
343 :param filter_q: dictionary to filter user list by name (username is also admited) and/or _id
344 :return: returns a list of users.
346 filt
= filter_q
or {}
348 filt
["username"] = filt
["name"]
350 users
= self
.db
.get_list("users", filt
)
354 prms
= user
.get("project_role_mappings")
355 projects
= user
.get("projects")
358 # add project_name and role_name. Generate projects for backward compatibility
360 project_id
= prm
["project"]
361 if project_id
not in project_id_name
:
362 pr
= self
.db
.get_one("projects", {BaseTopic
.id_field("projects", project_id
): project_id
},
364 project_id_name
[project_id
] = pr
["name"] if pr
else None
365 prm
["project_name"] = project_id_name
[project_id
]
366 if prm
["project_name"] not in projects
:
367 projects
.append(prm
["project_name"])
369 role_id
= prm
["role"]
370 if role_id
not in role_id_name
:
371 role
= self
.db
.get_one("roles", {BaseTopic
.id_field("roles", role_id
): role_id
},
373 role_id_name
[role_id
] = role
["name"] if role
else None
374 prm
["role_name"] = role_id_name
[role_id
]
375 user
["projects"] = projects
# for backward compatibility
377 # user created with an old version. Create a project_role mapping with role project_admin
378 user
["project_role_mappings"] = []
379 role
= self
.db
.get_one("roles", {BaseTopic
.id_field("roles", "project_admin"): "project_admin"})
380 for p_id_name
in projects
:
381 pr
= self
.db
.get_one("projects", {BaseTopic
.id_field("projects", p_id_name
): p_id_name
})
382 prm
= {"project": pr
["_id"],
383 "project_name": pr
["name"],
384 "role_name": "project_admin",
387 user
["project_role_mappings"].append(prm
)
389 user
["projects"] = []
390 user
["project_role_mappings"] = []
394 def get_project_list(self
, filter_q
={}):
398 :return: returns the list of projects.
400 return self
.db
.get_list("projects", filter_q
)
402 def create_project(self
, project_info
):
406 :param project: full project info.
407 :return: the internal id of the created project
408 :raises AuthconnOperationException: if project creation failed.
410 pid
= self
.db
.create("projects", project_info
)
413 def delete_project(self
, project_id
):
417 :param project_id: project identifier.
418 :raises AuthconnOperationException: if project deletion failed.
420 filter_q
= {BaseTopic
.id_field("projects", project_id
): project_id
}
421 r
= self
.db
.del_one("projects", filter_q
)
424 def update_project(self
, project_id
, project_info
):
426 Change the name of a project
428 :param project_id: project to be changed
429 :param project_info: full project info
431 :raises AuthconnOperationException: if project update failed.
433 self
.db
.set_one("projects", {BaseTopic
.id_field("projects", project_id
): project_id
}, project_info
)