X-Git-Url: https://osm.etsi.org/gitweb/?a=blobdiff_plain;f=osm_nbi%2Fauthconn_internal.py;h=0f414b13f620a730845c7531acef0a24df7a8dd3;hb=514b546acfeedf03541574394f934e06335f16ee;hp=da5e543583d417b6f95b334477771c3fe8886c79;hpb=23acf4001306e92a587de566be4bab00931104ba;p=osm%2FNBI.git diff --git a/osm_nbi/authconn_internal.py b/osm_nbi/authconn_internal.py index da5e543..0f414b1 100644 --- a/osm_nbi/authconn_internal.py +++ b/osm_nbi/authconn_internal.py @@ -24,17 +24,25 @@ AuthconnInternal implements implements the connector for OSM Internal Authentication Backend and leverages the RBAC model """ -__author__ = "Pedro de la Cruz Ramos , " \ - "Alfonso Tierno , " + "Alfonso Tierno user_info.get("_admin").get( + "account_expire_time" + ): + self.db.set_one( + self.users_collection, + {"_id": user_info["_id"]}, + {"_admin.user_status": "expired"}, + ) + break + + # To add "admin" user_status key while upgrading osm setup with feature enabled + if user_content.get("username") == "admin": + if self.config.get("user_management"): + self.db.set_one( + self.users_collection, + {"_id": user_content["_id"]}, + {"_admin.user_status": "always-active"}, + ) + + if not user_content.get("username") == "admin": + if self.config.get("user_management"): + if not user_content.get("_admin").get("account_expire_time"): + account_expire_time = now + 86400 * self.config.get( + "account_expire_days" + ) + self.db.set_one( + self.users_collection, + {"_id": user_content["_id"]}, + {"_admin.account_expire_time": account_expire_time}, + ) + else: + account_expire_time = user_content.get("_admin").get( + "account_expire_time" + ) + + if now > account_expire_time: + self.db.set_one( + self.users_collection, + {"_id": user_content["_id"]}, + {"_admin.user_status": "expired"}, + ) + raise AuthException( + "Account expired", http_code=HTTPStatus.UNAUTHORIZED + ) + + if user_content.get("_admin").get("user_status") == "locked": + raise AuthException( + "Failed to login as the account is locked due to MANY FAILED ATTEMPTS" + ) + elif user_content.get("_admin").get("user_status") == "expired": + raise AuthException( + "Failed to login as the account is expired" + ) - def authenticate(self, user, password, project=None, token_info=None): + salt = user_content["_admin"]["salt"] + shadow_password = sha256( + password.encode("utf-8") + salt.encode("utf-8") + ).hexdigest() + if shadow_password != user_content["password"]: + count = 1 + if user_content.get("_admin").get("retry_count") >= 0: + count += user_content.get("_admin").get("retry_count") + self.db.set_one( + self.users_collection, + {"_id": user_content["_id"]}, + {"_admin.retry_count": count}, + ) + self.logger.debug( + "Failed Authentications count: {}".format(count) + ) + + if user_content.get("username") == "admin": + user_content = None + else: + if not self.config.get("user_management"): + user_content = None + else: + if ( + user_content.get("_admin").get("retry_count") + >= self.config["max_pwd_attempt"] - 1 + ): + self.db.set_one( + self.users_collection, + {"_id": user_content["_id"]}, + {"_admin.user_status": "locked"}, + ) + raise AuthException( + "Failed to login as the account is locked due to MANY FAILED ATTEMPTS" + ) + else: + 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 - :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, @@ -141,30 +306,71 @@ class AuthconnInternal(Authconn): now = time() user_content = None + user = credentials.get("username") + password = credentials.get("password") + project = credentials.get("project_id") # 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) + cef_event( + self.cef_logger, + { + "name": "User login", + "sourceUserName": user, + "message": "Invalid username/password Project={} Outcome=Failure".format( + project + ), + "severity": "3", + }, + ) + self.logger.exception("{}".format(self.cef_logger)) + 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: raise AuthException("Invalid token", http_code=HTTPStatus.UNAUTHORIZED) else: - raise AuthException("Provide credentials: username/password or Authorization Bearer token", - http_code=HTTPStatus.UNAUTHORIZED) - - token_id = ''.join(random_choice('abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789') - for _ in range(0, 32)) + 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 + user_data = { + "_admin.last_token_time": now, + "_admin.retry_count": 0, + } + self.db.set_one( + self.users_collection, + {"_id": user_content["_id"]}, + user_data, + ) + + token_id = "".join( + random_choice( + "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" + ) + for _ in range(0, 32) + ) # projects = user_content.get("projects", []) prm_list = user_content.get("project_role_mappings", []) @@ -172,16 +378,23 @@ class AuthconnInternal(Authconn): if not project: project = prm_list[0]["project"] if prm_list else None if not project: - raise AuthException("can't find a default project for this user", http_code=HTTPStatus.UNAUTHORIZED) + raise AuthException( + "can't find a default project for this user", + http_code=HTTPStatus.UNAUTHORIZED, + ) 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: - raise AuthException("project {} not allowed for this user".format(project), - http_code=HTTPStatus.UNAUTHORIZED) + raise AuthException( + "project {} not allowed for this user".format(project), + http_code=HTTPStatus.UNAUTHORIZED, + ) # TODO remove admin, this vill be used by roles RBAC if project_name == "admin": @@ -194,30 +407,57 @@ class AuthconnInternal(Authconn): 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, - "expires": now + 3600, - "_id": token_id, - "id": token_id, - "project_id": proj["_id"], - "project_name": proj["name"], - "username": user_content["username"], - "user_id": user_content["_id"], - "admin": token_admin, - "roles": roles_list, - } - - self.token_cache[token_id] = new_token - self.db.create("tokens", new_token) + login_count = user_content.get("_admin").get("retry_count") + last_token_time = user_content.get("_admin").get("last_token_time") + + admin_show = False + user_show = False + if self.config.get("user_management"): + for role in roles_list: + role_id = role.get("id") + permission = self.db.get_one( + self.roles_collection, + {BaseTopic.id_field(self.roles_collection, role_id): role_id}, + ) + if permission.get("permissions")["admin"]: + if permission.get("permissions")["default"]: + admin_show = True + break + else: + user_show = True + new_token = { + "issued_at": now, + "expires": now + 3600, + "_id": token_id, + "id": token_id, + "project_id": proj["_id"], + "project_name": proj["name"], + "username": user_content["username"], + "user_id": user_content["_id"], + "admin": token_admin, + "roles": roles_list, + "login_count": login_count, + "last_login": last_token_time, + "admin_show": admin_show, + "user_show": user_show, + } + + self.db.create(self.tokens_collection, new_token) return deepcopy(new_token) def get_role_list(self, filter_q={}): @@ -226,7 +466,7 @@ class AuthconnInternal(Authconn): :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): """ @@ -239,7 +479,7 @@ class AuthconnInternal(Authconn): # 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): @@ -249,7 +489,9 @@ class AuthconnInternal(Authconn): :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(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): """ @@ -260,7 +502,7 @@ class AuthconnInternal(Authconn): :raises AuthconnOperationException: if user creation failed. """ rid = role_info["_id"] - self.db.set_one("roles", {"_id": rid}, role_info) # CONFIRM + self.db.set_one(self.roles_collection, {"_id": rid}, role_info) return {"_id": rid, "name": role_info["name"]} def create_user(self, user_info): @@ -273,12 +515,28 @@ class AuthconnInternal(Authconn): BaseTopic.format_on_new(user_info, make_public=False) salt = uuid4().hex user_info["_admin"]["salt"] = salt + user_info["_admin"]["user_status"] = "active" + present = time() + if not user_info["username"] == "admin": + if self.config.get("user_management"): + user_info["_admin"]["modified"] = present + user_info["_admin"]["password_expire_time"] = present + account_expire_time = present + 86400 * self.config.get( + "account_expire_days" + ) + user_info["_admin"]["account_expire_time"] = account_expire_time + + user_info["_admin"]["retry_count"] = 0 + user_info["_admin"]["last_token_time"] = present if "password" in user_info: - user_info["password"] = sha256(user_info["password"].encode('utf-8') + salt.encode('utf-8')).hexdigest() + user_info["password"] = sha256( + user_info["password"].encode("utf-8") + salt.encode("utf-8") + ).hexdigest() + user_info["_admin"]["password_history"] = {salt: user_info["password"]} # "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): @@ -288,7 +546,104 @@ class AuthconnInternal(Authconn): :param user_info: user info modifications """ uid = user_info["_id"] - user_data = self.db.get_one("users", {BaseTopic.id_field("users", uid): uid}) + old_pwd = user_info.get("old_password") + unlock = user_info.get("unlock") + renew = user_info.get("renew") + permission_id = user_info.get("system_admin_id") + + user_data = self.db.get_one( + self.users_collection, {BaseTopic.id_field("users", uid): uid} + ) + if old_pwd: + salt = user_data["_admin"]["salt"] + shadow_password = sha256( + old_pwd.encode("utf-8") + salt.encode("utf-8") + ).hexdigest() + if shadow_password != user_data["password"]: + raise AuthconnConflictException( + "Incorrect password", http_code=HTTPStatus.CONFLICT + ) + # Unlocking the user + if unlock: + system_user = None + unlock_state = False + if not permission_id: + raise AuthconnConflictException( + "system_admin_id is the required field to unlock the user", + http_code=HTTPStatus.CONFLICT, + ) + else: + system_user = self.db.get_one( + self.users_collection, + { + BaseTopic.id_field( + self.users_collection, permission_id + ): permission_id + }, + ) + mapped_roles = system_user.get("project_role_mappings") + for role in mapped_roles: + role_id = role.get("role") + role_assigned = self.db.get_one( + self.roles_collection, + {BaseTopic.id_field(self.roles_collection, role_id): role_id}, + ) + if role_assigned.get("permissions")["admin"]: + if role_assigned.get("permissions")["default"]: + user_data["_admin"]["retry_count"] = 0 + user_data["_admin"]["user_status"] = "active" + unlock_state = True + break + if not unlock_state: + raise AuthconnConflictException( + "User '{}' does not have the privilege to unlock the user".format( + permission_id + ), + http_code=HTTPStatus.CONFLICT, + ) + # Renewing the user + if renew: + system_user = None + renew_state = False + if not permission_id: + raise AuthconnConflictException( + "system_admin_id is the required field to renew the user", + http_code=HTTPStatus.CONFLICT, + ) + else: + system_user = self.db.get_one( + self.users_collection, + { + BaseTopic.id_field( + self.users_collection, permission_id + ): permission_id + }, + ) + mapped_roles = system_user.get("project_role_mappings") + for role in mapped_roles: + role_id = role.get("role") + role_assigned = self.db.get_one( + self.roles_collection, + {BaseTopic.id_field(self.roles_collection, role_id): role_id}, + ) + if role_assigned.get("permissions")["admin"]: + if role_assigned.get("permissions")["default"]: + present = time() + account_expire = ( + present + 86400 * self.config["account_expire_days"] + ) + user_data["_admin"]["modified"] = present + user_data["_admin"]["account_expire_time"] = account_expire + user_data["_admin"]["user_status"] = "active" + renew_state = True + break + if not renew_state: + raise AuthconnConflictException( + "User '{}' does not have the privilege to renew the user".format( + permission_id + ), + http_code=HTTPStatus.CONFLICT, + ) BaseTopic.format_on_edit(user_data, user_info) # User Name usnm = user_info.get("username") @@ -296,12 +651,49 @@ class AuthconnInternal(Authconn): user_data["username"] = usnm # If password is given and is not already encripted pswd = user_info.get("password") - if pswd and (len(pswd) != 64 or not re.match('[a-fA-F0-9]*', pswd)): # TODO: Improve check? + if pswd and ( + len(pswd) != 64 or not re.match("[a-fA-F0-9]*", pswd) + ): # TODO: Improve check? + cef_event( + self.cef_logger, + { + "name": "Change Password", + "sourceUserName": user_data["username"], + "message": "Changing Password for user, Outcome=Success", + "severity": "2", + }, + ) + self.logger.info("{}".format(self.cef_logger)) salt = uuid4().hex if "_admin" not in user_data: user_data["_admin"] = {} + if user_data.get("_admin").get("password_history"): + old_pwds = user_data.get("_admin").get("password_history") + else: + old_pwds = {} + for k, v in old_pwds.items(): + shadow_password = sha256( + pswd.encode("utf-8") + k.encode("utf-8") + ).hexdigest() + if v == shadow_password: + raise AuthconnConflictException( + "Password is used before", http_code=HTTPStatus.CONFLICT + ) user_data["_admin"]["salt"] = salt - user_data["password"] = sha256(pswd.encode('utf-8') + salt.encode('utf-8')).hexdigest() + user_data["password"] = sha256( + pswd.encode("utf-8") + salt.encode("utf-8") + ).hexdigest() + if len(old_pwds) >= 3: + old_pwds.pop(list(old_pwds.keys())[0]) + old_pwds.update({salt: user_data["password"]}) + user_data["_admin"]["password_history"] = old_pwds + if not user_data["username"] == "admin": + if self.config.get("user_management"): + present = time() + if self.config.get("pwd_expire_days"): + expire = present + 86400 * self.config.get("pwd_expire_days") + user_data["_admin"]["modified"] = present + user_data["_admin"]["password_expire_time"] = expire # Project-Role Mappings # TODO: Check that user_info NEVER includes "project_role_mappings" if "project_role_mappings" not in user_data: @@ -312,12 +704,18 @@ class AuthconnInternal(Authconn): for pidf in ["project", "project_name"]: for ridf in ["role", "role_name"]: try: - user_data["project_role_mappings"].remove({"role": prm[ridf], "project": prm[pidf]}) + user_data["project_role_mappings"].remove( + {"role": prm[ridf], "project": prm[pidf]} + ) except KeyError: 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(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(self.tokens_collection, {idf: uid}) def delete_user(self, user_id): """ @@ -326,21 +724,26 @@ class AuthconnInternal(Authconn): :param user_id: user identifier. :raises AuthconnOperationException: if user deletion failed. """ - self.db.del_one("users", {"_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): """ 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"] - users = self.db.get_list("users", filt) + 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(self.users_collection, filt) project_id_name = {} role_id_name = {} for user in users: @@ -352,8 +755,11 @@ class AuthconnInternal(Authconn): 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}, - fail_on_empty=False) + 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] if prm["project_name"] not in projects: @@ -361,22 +767,32 @@ class AuthconnInternal(Authconn): 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 = 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] 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"}) + 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}) - prm = {"project": pr["_id"], - "project_name": pr["name"], - "role_name": "project_admin", - "role": role["_id"] - } + 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", + "role": role["_id"], + } user["project_role_mappings"].append(prm) else: user["projects"] = [] @@ -390,7 +806,7 @@ class AuthconnInternal(Authconn): :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): """ @@ -400,7 +816,7 @@ class AuthconnInternal(Authconn): :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): @@ -410,8 +826,10 @@ class AuthconnInternal(Authconn): :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(self.projects_collection, {idf: project_id}) + idf = "project_id" if idf == "_id" else "project_name" + self.db.del_list(self.tokens_collection, {idf: project_id}) return r def update_project(self, project_id, project_info): @@ -423,4 +841,8 @@ class AuthconnInternal(Authconn): :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, + )