OSM Internal Authentication Backend and leverages the RBAC model
"""
-__author__ = "Pedro de la Cruz Ramos <pdelacruzramos@altran.com>"
+__author__ = "Pedro de la Cruz Ramos <pdelacruzramos@altran.com>, " \
+ "Alfonso Tierno <alfonso.tiernosepulveda@telefoncia.com"
__date__ = "$06-jun-2019 11:16:08$"
-from authconn import Authconn, AuthException # , AuthconnOperationException
-from osm_common.dbbase import DbException
-from base_topic import BaseTopic
-
import logging
import re
-from time import time
+
+from osm_nbi.authconn import Authconn, AuthException # , AuthconnOperationException
+from osm_common.dbbase import DbException
+from osm_nbi.base_topic import BaseTopic
+from osm_nbi.validation import is_valid_uuid
+from time import time, sleep
from http import HTTPStatus
from uuid import uuid4
from hashlib import sha256
class AuthconnInternal(Authconn):
- def __init__(self, config, db, token_cache):
- Authconn.__init__(self, config, db, token_cache)
+ token_time_window = 2 # seconds
+ token_delay = 1 # seconds to wait upon second request within time window
+ def __init__(self, config, db, role_permissions):
+ Authconn.__init__(self, config, db, role_permissions)
self.logger = logging.getLogger("nbi.authenticator.internal")
- # Get Configuration
- # self.xxx = config.get("xxx", "default")
-
self.db = db
- self.token_cache = token_cache
+ # self.msg = msg
+ # self.token_cache = token_cache
# To be Confirmed
- self.auth = None
self.sess = None
def validate_token(self, token):
if not token:
raise AuthException("Needed a token or Authorization HTTP header", http_code=HTTPStatus.UNAUTHORIZED)
- # try to get from cache first
now = time()
- token_info = self.token_cache.get(token)
- if token_info and token_info["expires"] < now:
- # delete token. MUST be done with care, as another thread maybe already delete it. Do not use del
- self.token_cache.pop(token, None)
- token_info = None
# get from database if not in cache
- if not token_info:
- token_info = self.db.get_one("tokens", {"_id": token})
- if token_info["expires"] < now:
- raise AuthException("Expired Token or Authorization HTTP header", http_code=HTTPStatus.UNAUTHORIZED)
+ # if not token_info:
+ token_info = self.db.get_one("tokens", {"_id": token})
+ if token_info["expires"] < now:
+ raise AuthException("Expired Token or Authorization HTTP header", http_code=HTTPStatus.UNAUTHORIZED)
return token_info
else:
raise
except AuthException:
- if self.config["global"].get("test.user_not_authorized"):
- return {"id": "fake-token-id-for-test",
- "project_id": self.config["global"].get("test.project_not_authorized", "admin"),
- "username": self.config["global"]["test.user_not_authorized"], "admin": True}
- else:
- raise
+ raise
except Exception:
self.logger.exception("Error during token validation using internal backend")
raise AuthException("Error during token validation using internal backend",
:param token: token to be revoked
"""
try:
- self.token_cache.pop(token, None)
+ # self.token_cache.pop(token, None)
self.db.del_one("tokens", {"_id": token})
return True
except DbException as e:
raise AuthException("Token '{}' not found".format(token), http_code=HTTPStatus.NOT_FOUND)
else:
# raise
- msg = "Error during token revocation using internal backend"
- self.logger.exception(msg)
- raise AuthException(msg, http_code=HTTPStatus.UNAUTHORIZED)
+ exmsg = "Error during token revocation using internal backend"
+ self.logger.exception(exmsg)
+ raise AuthException(exmsg, http_code=HTTPStatus.UNAUTHORIZED)
- def authenticate(self, user, password, project=None, token_info=None):
+ def authenticate(self, credentials, token_info=None):
"""
Authenticate a user using username/password or previous token_info plus project; its creates a new token
- :param user: user: name, id or None
- :param password: password or None
- :param project: name, id, or None. If None first found project will be used to get an scope token
+ :param credentials: dictionary that contains:
+ username: name, id or None
+ password: password or None
+ project_id: name, id, or None. If None first found project will be used to get an scope token
+ other items are allowed and ignored
:param token_info: previous token_info to obtain authorization
- :param remote: remote host information
:return: the scoped token info or raises an exception. The token is a dictionary with:
_id: token string id,
username: username,
now = time()
user_content = None
+ user = credentials.get("username")
+ password = credentials.get("password")
+ project = credentials.get("project_id")
# Try using username/password
if user:
raise AuthException("Provide credentials: username/password or Authorization Bearer token",
http_code=HTTPStatus.UNAUTHORIZED)
+ # Delay upon second request within time window
+ if now - user_content["_admin"].get("last_token_time", 0) < self.token_time_window:
+ sleep(self.token_delay)
+ # user_content["_admin"]["last_token_time"] = now
+ # self.db.replace("users", user_content["_id"], user_content) # might cause race conditions
+ self.db.set_one("users", {"_id": user_content["_id"]}, {"_admin.last_token_time": now})
+
token_id = ''.join(random_choice('abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789')
for _ in range(0, 32))
"roles": roles_list,
}
- self.token_cache[token_id] = new_token
self.db.create("tokens", new_token)
return deepcopy(new_token)
:param role_id: role identifier.
:raises AuthconnOperationException: if role deletion failed.
"""
- return self.db.del_one("roles", {"_id": role_id})
+ rc = self.db.del_one("roles", {"_id": role_id})
+ self.db.del_list("tokens", {"roles.id": role_id})
+ return rc
def update_role(self, role_info):
"""
:raises AuthconnOperationException: if user creation failed.
"""
rid = role_info["_id"]
- self.db.set_one("roles", {"_id": rid}, role_info) # CONFIRM
+ self.db.set_one("roles", {"_id": rid}, role_info)
return {"_id": rid, "name": role_info["name"]}
def create_user(self, user_info):
pass
except ValueError:
pass
- self.db.set_one("users", {BaseTopic.id_field("users", uid): uid}, user_data) # CONFIRM
+ idf = BaseTopic.id_field("users", uid)
+ self.db.set_one("users", {idf: uid}, user_data)
+ if user_info.get("remove_project_role_mappings"):
+ idf = "user_id" if idf == "_id" else idf
+ self.db.del_list("tokens", {idf: uid})
def delete_user(self, user_id):
"""
:raises AuthconnOperationException: if user deletion failed.
"""
self.db.del_one("users", {"_id": user_id})
+ self.db.del_list("tokens", {"user_id": user_id})
return True
def get_user_list(self, filter_q=None):
"""
Get user list.
- :param filter_q: dictionary to filter user list by name (username is also admited) and/or _id
+ :param filter_q: dictionary to filter user list by:
+ name (username is also admitted). If a user id is equal to the filter name, it is also provided
+ other
:return: returns a list of users.
"""
filt = filter_q or {}
- if "name" in filt:
- filt["username"] = filt["name"]
- del filt["name"]
+ if "name" in filt: # backward compatibility
+ filt["username"] = filt.pop("name")
+ if filt.get("username") and is_valid_uuid(filt["username"]):
+ # username cannot be a uuid. If this is the case, change from username to _id
+ filt["_id"] = filt.pop("username")
users = self.db.get_list("users", filt)
+ project_id_name = {}
+ role_id_name = {}
for user in users:
- projects = []
- projs_with_roles = []
- prms = user.get("project_role_mappings", [])
- for prm in prms:
- if prm["project"] not in projects:
- projects.append(prm["project"])
- for project in projects:
- roles = []
- roles_for_proj = []
+ prms = user.get("project_role_mappings")
+ projects = user.get("projects")
+ if prms:
+ projects = []
+ # add project_name and role_name. Generate projects for backward compatibility
for prm in prms:
- if prm["project"] == project and prm["role"] not in roles:
- role = prm["role"]
- roles.append(role)
- rl = self.db.get_one("roles", {BaseTopic.id_field("roles", role): role})
- roles_for_proj.append({"name": rl["name"], "_id": rl["_id"], "id": rl["_id"]})
- try:
- pr = self.db.get_one("projects", {BaseTopic.id_field("projects", project): project})
- projs_with_roles.append({"name": pr["name"], "_id": pr["_id"], "id": pr["_id"],
- "roles": roles_for_proj})
- except Exception as e:
- self.logger.exception("Error during user listing using internal backend: {}".format(e))
- user["projects"] = projs_with_roles
- if "project_role_mappings" in user:
- del user["project_role_mappings"]
+ project_id = prm["project"]
+ if project_id not in project_id_name:
+ pr = self.db.get_one("projects", {BaseTopic.id_field("projects", project_id): project_id},
+ fail_on_empty=False)
+ project_id_name[project_id] = pr["name"] if pr else None
+ prm["project_name"] = project_id_name[project_id]
+ if prm["project_name"] not in projects:
+ projects.append(prm["project_name"])
+
+ role_id = prm["role"]
+ if role_id not in role_id_name:
+ role = self.db.get_one("roles", {BaseTopic.id_field("roles", role_id): role_id},
+ fail_on_empty=False)
+ role_id_name[role_id] = role["name"] if role else None
+ prm["role_name"] = role_id_name[role_id]
+ user["projects"] = projects # for backward compatibility
+ elif projects:
+ # user created with an old version. Create a project_role mapping with role project_admin
+ user["project_role_mappings"] = []
+ role = self.db.get_one("roles", {BaseTopic.id_field("roles", "project_admin"): "project_admin"})
+ for p_id_name in projects:
+ pr = self.db.get_one("projects", {BaseTopic.id_field("projects", p_id_name): p_id_name})
+ prm = {"project": pr["_id"],
+ "project_name": pr["name"],
+ "role_name": "project_admin",
+ "role": role["_id"]
+ }
+ user["project_role_mappings"].append(prm)
+ else:
+ user["projects"] = []
+ user["project_role_mappings"] = []
+
return users
def get_project_list(self, filter_q={}):
:param project_id: project identifier.
:raises AuthconnOperationException: if project deletion failed.
"""
- filter_q = {BaseTopic.id_field("projects", project_id): project_id}
- r = self.db.del_one("projects", filter_q)
+ idf = BaseTopic.id_field("projects", project_id)
+ r = self.db.del_one("projects", {idf: project_id})
+ idf = "project_id" if idf == "_id" else "project_name"
+ self.db.del_list("tokens", {idf: project_id})
return r
def update_project(self, project_id, project_info):