token_time_window = 2 # seconds
token_delay = 1 # seconds to wait upon second request within time window
+ users_collection = "users"
+ roles_collection = "roles"
+ projects_collection = "projects"
+ tokens_collection = "tokens"
+
def __init__(self, config, db, role_permissions):
Authconn.__init__(self, config, db, role_permissions)
self.logger = logging.getLogger("nbi.authenticator.internal")
# get from database if not in cache
# if not token_info:
- token_info = self.db.get_one("tokens", {"_id": token})
+ token_info = self.db.get_one(self.tokens_collection, {"_id": token})
if token_info["expires"] < now:
raise AuthException("Expired Token or Authorization HTTP header", http_code=HTTPStatus.UNAUTHORIZED)
"""
try:
# self.token_cache.pop(token, None)
- self.db.del_one("tokens", {"_id": token})
+ self.db.del_one(self.tokens_collection, {"_id": token})
return True
except DbException as e:
if e.http_code == HTTPStatus.NOT_FOUND:
self.logger.exception(exmsg)
raise AuthException(exmsg, http_code=HTTPStatus.UNAUTHORIZED)
+ def validate_user(self, user, password):
+ """
+ Validate username and password via appropriate backend.
+ :param user: username of the user.
+ :param password: password to be validated.
+ """
+ user_rows = self.db.get_list(self.users_collection, {BaseTopic.id_field("users", user): user})
+ user_content = None
+ if user_rows:
+ user_content = user_rows[0]
+ salt = user_content["_admin"]["salt"]
+ shadow_password = sha256(password.encode('utf-8') + salt.encode('utf-8')).hexdigest()
+ if shadow_password != user_content["password"]:
+ user_content = None
+ return user_content
+
def authenticate(self, credentials, token_info=None):
"""
Authenticate a user using username/password or previous token_info plus project; its creates a new token
# Try using username/password
if user:
- user_rows = self.db.get_list("users", {BaseTopic.id_field("users", user): user})
- if user_rows:
- user_content = user_rows[0]
- salt = user_content["_admin"]["salt"]
- shadow_password = sha256(password.encode('utf-8') + salt.encode('utf-8')).hexdigest()
- if shadow_password != user_content["password"]:
- user_content = None
+ user_content = self.validate_user(user, password)
if not user_content:
raise AuthException("Invalid username/password", http_code=HTTPStatus.UNAUTHORIZED)
+ if not user_content.get("_admin", None):
+ raise AuthException("No default project for this user.", http_code=HTTPStatus.UNAUTHORIZED)
elif token_info:
- user_rows = self.db.get_list("users", {"username": token_info["username"]})
+ user_rows = self.db.get_list(self.users_collection, {"username": token_info["username"]})
if user_rows:
user_content = user_rows[0]
else:
else:
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})
+ self.db.set_one(self.users_collection,
+ {"_id": user_content["_id"]}, {"_admin.last_token_time": now})
token_id = ''.join(random_choice('abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789')
for _ in range(0, 32))
projects = [prm["project"] for prm in prm_list]
- proj = self.db.get_one("projects", {BaseTopic.id_field("projects", project): project})
+ proj = self.db.get_one(self.projects_collection,
+ {BaseTopic.id_field("projects", project): project})
project_name = proj["name"]
project_id = proj["_id"]
if project_name not in projects and project_id not in projects:
roles_list = []
for prm in prm_list:
if prm["project"] in [project_id, project_name]:
- role = self.db.get_one("roles", {BaseTopic.id_field("roles", prm["role"]): prm["role"]})
+ role = self.db.get_one(self.roles_collection,
+ {BaseTopic.id_field("roles", prm["role"]): prm["role"]})
rid = role["_id"]
if rid not in roles:
rnm = role["name"]
roles.append(rid)
roles_list.append({"name": rnm, "id": rid})
if not roles_list:
- rid = self.db.get_one("roles", {"name": "project_admin"})["_id"]
+ rid = self.db.get_one(self.roles_collection, {"name": "project_admin"})["_id"]
roles_list = [{"name": "project_admin", "id": rid}]
new_token = {"issued_at": now,
"roles": roles_list,
}
- self.db.create("tokens", new_token)
+ self.db.create(self.tokens_collection, new_token)
return deepcopy(new_token)
def get_role_list(self, filter_q={}):
:return: returns the list of roles.
"""
- return self.db.get_list("roles", filter_q)
+ return self.db.get_list(self.roles_collection, filter_q)
def create_role(self, role_info):
"""
# TODO: Check that role name does not exist ?
rid = str(uuid4())
role_info["_id"] = rid
- rid = self.db.create("roles", role_info)
+ rid = self.db.create(self.roles_collection, role_info)
return rid
def delete_role(self, role_id):
:param role_id: role identifier.
:raises AuthconnOperationException: if role deletion failed.
"""
- rc = self.db.del_one("roles", {"_id": role_id})
- self.db.del_list("tokens", {"roles.id": role_id})
+ rc = self.db.del_one(self.roles_collection, {"_id": role_id})
+ self.db.del_list(self.tokens_collection, {"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)
+ self.db.set_one(self.roles_collection, {"_id": rid}, role_info)
return {"_id": rid, "name": role_info["name"]}
def create_user(self, user_info):
# "projects" are not stored any more
if "projects" in user_info:
del user_info["projects"]
- self.db.create("users", user_info)
+ self.db.create(self.users_collection, user_info)
return {"username": user_info["username"], "_id": user_info["_id"]}
def update_user(self, user_info):
:param user_info: user info modifications
"""
uid = user_info["_id"]
- user_data = self.db.get_one("users", {BaseTopic.id_field("users", uid): uid})
+ user_data = self.db.get_one(self.users_collection, {BaseTopic.id_field("users", uid): uid})
BaseTopic.format_on_edit(user_data, user_info)
# User Name
usnm = user_info.get("username")
except ValueError:
pass
idf = BaseTopic.id_field("users", uid)
- self.db.set_one("users", {idf: uid}, user_data)
+ self.db.set_one(self.users_collection, {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})
+ self.db.del_list(self.tokens_collection, {idf: uid})
def delete_user(self, user_id):
"""
:param user_id: user identifier.
:raises AuthconnOperationException: if user deletion failed.
"""
- self.db.del_one("users", {"_id": user_id})
- self.db.del_list("tokens", {"user_id": user_id})
+ self.db.del_one(self.users_collection, {"_id": user_id})
+ self.db.del_list(self.tokens_collection, {"user_id": user_id})
return True
def get_user_list(self, filter_q=None):
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)
+ users = self.db.get_list(self.users_collection, filt)
project_id_name = {}
role_id_name = {}
for user in users:
for prm in prms:
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},
+ pr = self.db.get_one(self.projects_collection,
+ {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]
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},
+ role = self.db.get_one(self.roles_collection,
+ {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]
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"})
+ role = self.db.get_one(self.roles_collection,
+ {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})
+ pr = self.db.get_one(self.projects_collection,
+ {BaseTopic.id_field("projects", p_id_name): p_id_name})
prm = {"project": pr["_id"],
"project_name": pr["name"],
"role_name": "project_admin",
:return: returns the list of projects.
"""
- return self.db.get_list("projects", filter_q)
+ return self.db.get_list(self.projects_collection, filter_q)
def create_project(self, project_info):
"""
:return: the internal id of the created project
:raises AuthconnOperationException: if project creation failed.
"""
- pid = self.db.create("projects", project_info)
+ pid = self.db.create(self.projects_collection, project_info)
return pid
def delete_project(self, project_id):
:raises AuthconnOperationException: if project deletion failed.
"""
idf = BaseTopic.id_field("projects", project_id)
- r = self.db.del_one("projects", {idf: project_id})
+ r = self.db.del_one(self.projects_collection, {idf: project_id})
idf = "project_id" if idf == "_id" else "project_name"
- self.db.del_list("tokens", {idf: project_id})
+ self.db.del_list(self.tokens_collection, {idf: project_id})
return r
def update_project(self, project_id, project_info):
:return: None
:raises AuthconnOperationException: if project update failed.
"""
- self.db.set_one("projects", {BaseTopic.id_field("projects", project_id): project_id}, project_info)
+ self.db.set_one(self.projects_collection, {BaseTopic.id_field("projects", project_id): project_id},
+ project_info)
--- /dev/null
+# -*- coding: utf-8 -*-
+
+# Copyright 2020 TATA ELXSI
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+#
+# For those usages not covered by the Apache License, Version 2.0 please
+# contact: saikiran.k@tataelxsi.co.in
+##
+
+
+"""
+AuthconnTacacs implements implements the connector for TACACS.
+Leverages AuthconnInternal for token lifecycle management and the RBAC model.
+
+When NBI bootstraps, it tries to create admin user with admin role associated to admin project.
+Hence, the TACACS server should contain admin user.
+"""
+
+__author__ = "K Sai Kiran <saikiran.k@tataelxsi.co.in>"
+__date__ = "$11-Nov-2020 11:04:00$"
+
+
+from osm_nbi.authconn import Authconn, AuthException
+from osm_nbi.authconn_internal import AuthconnInternal
+from osm_nbi.base_topic import BaseTopic
+
+import logging
+from time import time
+from http import HTTPStatus
+
+# TACACS+ Library
+from tacacs_plus.client import TACACSClient
+
+
+class AuthconnTacacs(AuthconnInternal):
+ token_time_window = 2
+ token_delay = 1
+
+ tacacs_def_port = 49
+ tacacs_def_timeout = 10
+ users_collection = "users_tacacs"
+ roles_collection = "roles_tacacs"
+ projects_collection = "projects_tacacs"
+ tokens_collection = "tokens_tacacs"
+
+ def __init__(self, config, db, role_permissions):
+ """
+ Constructor to initialize db and TACACS server attributes to members.
+ """
+ Authconn.__init__(self, config, db, role_permissions)
+ self.logger = logging.getLogger("nbi.authenticator.tacacs")
+ self.db = db
+ self.tacacs_host = config["tacacs_host"]
+ self.tacacs_secret = config["tacacs_secret"]
+ self.tacacs_port = config["tacacs_port"] if config.get("tacacs_port") else self.tacacs_def_port
+ self.tacacs_timeout = config["tacacs_timeout"] if config.get("tacacs_timeout") else self.tacacs_def_timeout
+ self.tacacs_cli = TACACSClient(self.tacacs_host, self.tacacs_port, self.tacacs_secret,
+ self.tacacs_timeout)
+
+ def validate_user(self, user, password):
+ """
+ """
+ now = time()
+ try:
+ tacacs_authen = self.tacacs_cli.authenticate(user, password)
+ except Exception as e:
+ raise AuthException("TACACS server error: {}".format(e), http_code=HTTPStatus.UNAUTHORIZED)
+ user_content = None
+ user_rows = self.db.get_list(self.users_collection, {BaseTopic.id_field("users", user): user})
+ if not tacacs_authen.valid:
+ if user_rows:
+ # To remove TACACS stale user from system.
+ self.delete_user(user_rows[0][BaseTopic.id_field("users", user)])
+ return user_content
+ if user_rows:
+ user_content = user_rows[0]
+ else:
+ new_user = {'username': user,
+ 'password': password,
+ '_admin': {
+ 'created': now,
+ 'modified': now
+ },
+ 'project_role_mappings': []
+ }
+ user_content = self.create_user(new_user)
+ return user_content
+
+ def create_user(self, user_info):
+ """
+ Validates user credentials in TACACS and add user.
+
+ :param user_info: Full user information in dict.
+ :return: returns username and id if credentails are valid. Otherwise, raise exception
+ """
+ BaseTopic.format_on_new(user_info, make_public=False)
+ try:
+ authen = self.tacacs_cli.authenticate(user_info["username"], user_info["password"])
+ if authen.valid:
+ user_info.pop("password")
+ self.db.create(self.users_collection, user_info)
+ else:
+ raise AuthException("TACACS server error: Invalid credentials", http_code=HTTPStatus.FORBIDDEN)
+ except Exception as e:
+ raise AuthException("TACACS server error: {}".format(e), http_code=HTTPStatus.BAD_REQUEST)
+ return {"username": user_info["username"], "_id": user_info["_id"]}
+
+ def update_user(self, user_info):
+ """
+ Updates user information, in particular for add/remove of project and role mappings.
+ Does not allow change of username or password.
+
+ :param user_info: Full user information in dict.
+ :return: returns None for successful add/remove of project and role map.
+ """
+ if(user_info.get("username")):
+ raise AuthException("Can not update username of this user", http_code=HTTPStatus.FORBIDDEN)
+ if(user_info.get("password")):
+ raise AuthException("Can not update password of this user", http_code=HTTPStatus.FORBIDDEN)
+ super(AuthconnTacacs, self).update_user(user_info)