2024-10-28 21:57:23 +00:00
|
|
|
from typing import Optional
|
|
|
|
|
2024-11-19 17:50:43 +00:00
|
|
|
import os
|
|
|
|
import subprocess
|
2024-11-02 23:15:51 +00:00
|
|
|
import requests
|
2024-11-19 17:50:43 +00:00
|
|
|
from contextlib import contextmanager
|
2024-11-02 23:15:51 +00:00
|
|
|
|
2024-11-11 00:33:17 +00:00
|
|
|
from selfprivacy_api.utils import get_domain
|
2024-11-19 17:50:43 +00:00
|
|
|
from selfprivacy_api.utils.redis_pool import RedisPool
|
2024-11-11 17:37:17 +00:00
|
|
|
from selfprivacy_api.models.user import UserDataUser, UserDataUserOrigin
|
2024-10-28 21:57:23 +00:00
|
|
|
from selfprivacy_api.repositories.users.abstract_user_repository import (
|
|
|
|
AbstractUserRepository,
|
|
|
|
)
|
|
|
|
|
2024-11-11 16:50:52 +00:00
|
|
|
KANIDM_URL = "https://127.0.0.1:3013"
|
2024-11-19 17:50:43 +00:00
|
|
|
|
|
|
|
redis = RedisPool().get_connection()
|
|
|
|
|
|
|
|
|
|
|
|
@contextmanager
|
|
|
|
def temporary_env_var(key, value):
|
|
|
|
"""
|
|
|
|
A context manager for temporarily setting an environment variable
|
|
|
|
with automatic cleanup after exiting the block, even in case of an error.
|
|
|
|
"""
|
|
|
|
os.environ[key] = value
|
|
|
|
try:
|
|
|
|
yield
|
|
|
|
finally:
|
|
|
|
del os.environ[key]
|
|
|
|
|
|
|
|
|
|
|
|
class KanidmAdminToken:
|
|
|
|
@staticmethod
|
|
|
|
def get() -> str:
|
|
|
|
kanidm_admin_token = redis.get("kanidm:token")
|
|
|
|
|
|
|
|
if kanidm_admin_token is None:
|
|
|
|
kanidm_admin_password = redis.get("kanidm:password") # type: ignore
|
|
|
|
|
|
|
|
if kanidm_admin_password is None:
|
|
|
|
kanidm_admin_password = (
|
|
|
|
KanidmAdminToken.reset_and_save_idm_admin_password()
|
|
|
|
)
|
|
|
|
|
|
|
|
kanidm_admin_token = KanidmAdminToken.create_and_save_token(
|
|
|
|
kanidm_admin_password=kanidm_admin_password
|
|
|
|
)
|
|
|
|
|
|
|
|
return kanidm_admin_token
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
def create_and_save_token(kanidm_admin_password: str) -> str:
|
|
|
|
with temporary_env_var(key="KANIDM_PASSWORD", value=kanidm_admin_password):
|
2024-11-21 15:42:02 +00:00
|
|
|
# kanidm_admin_token = subprocess.check_output(
|
|
|
|
# [
|
|
|
|
# "kanidm",
|
|
|
|
# "service-account",
|
|
|
|
# "api-token",
|
|
|
|
# "generate",
|
|
|
|
# "--rw",
|
|
|
|
# "selfprivacy",
|
|
|
|
# "token2",
|
|
|
|
# ]
|
|
|
|
# )
|
|
|
|
kanidm_admin_token = (
|
|
|
|
subprocess.check_output(
|
|
|
|
"kanidm service-account api-token generate --rw selfprivacy token2",
|
|
|
|
shell=True,
|
|
|
|
stderr=subprocess.STDOUT,
|
|
|
|
)
|
|
|
|
.decode("utf-8")
|
|
|
|
.strip()
|
2024-11-19 17:50:43 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
redis.set("kanidm:token", kanidm_admin_token)
|
|
|
|
return kanidm_admin_token
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
def reset_and_save_idm_admin_password() -> str:
|
2024-11-21 15:42:02 +00:00
|
|
|
# new_kanidm_admin_password = subprocess.check_output(
|
|
|
|
# [
|
|
|
|
# "kanidmd",
|
|
|
|
# "recover-account",
|
|
|
|
# "-c",
|
|
|
|
# "/etc/kanidm/server.toml",
|
|
|
|
# "idm_admin",
|
|
|
|
# "-o",
|
|
|
|
# "json",
|
|
|
|
# "2>/dev/null",
|
|
|
|
# "|",
|
|
|
|
# "grep",
|
|
|
|
# "'{\"password'",
|
|
|
|
# "|",
|
|
|
|
# "jq",
|
|
|
|
# "-r",
|
|
|
|
# ".password",
|
|
|
|
# ]
|
|
|
|
# ).decode("utf-8")
|
|
|
|
new_kanidm_admin_password = (
|
|
|
|
subprocess.check_output(
|
|
|
|
"kanidmd recover-account -c /etc/kanidm/server.toml idm_admin -o json 2>/dev/null | grep '{\"password' | jq -r .password",
|
|
|
|
shell=True,
|
|
|
|
stderr=subprocess.STDOUT,
|
|
|
|
)
|
|
|
|
.decode("utf-8")
|
|
|
|
.strip()
|
|
|
|
)
|
2024-11-19 17:50:43 +00:00
|
|
|
|
|
|
|
redis.set("kanidm:password", new_kanidm_admin_password)
|
|
|
|
return new_kanidm_admin_password
|
2024-11-02 23:15:51 +00:00
|
|
|
|
|
|
|
|
|
|
|
class KanidmQueryError(Exception):
|
2024-11-11 08:41:30 +00:00
|
|
|
"""Error occurred during kanidm query"""
|
|
|
|
|
2024-10-28 21:57:23 +00:00
|
|
|
|
|
|
|
class KanidmUserRepository(AbstractUserRepository):
|
2024-11-02 23:15:51 +00:00
|
|
|
@staticmethod
|
2024-11-11 00:33:17 +00:00
|
|
|
def _send_query(endpoint: str, method: str = "GET", data=None):
|
2024-11-02 23:15:51 +00:00
|
|
|
request_method = getattr(requests, method.lower(), None)
|
2024-11-11 16:36:16 +00:00
|
|
|
full_endpoint = f"{KANIDM_URL}/v1/{endpoint}"
|
2024-11-02 23:15:51 +00:00
|
|
|
|
|
|
|
try:
|
|
|
|
response = request_method(
|
2024-11-11 16:36:16 +00:00
|
|
|
full_endpoint,
|
2024-11-11 00:33:17 +00:00
|
|
|
json=data,
|
|
|
|
headers={
|
2024-11-20 13:07:42 +00:00
|
|
|
"Authorization": f"Bearer {KanidmAdminToken.get()}",
|
2024-11-11 00:33:17 +00:00
|
|
|
"Content-Type": "application/json",
|
|
|
|
},
|
2024-11-02 23:15:51 +00:00
|
|
|
timeout=0.8, # TODO: change timeout
|
2024-11-11 17:09:14 +00:00
|
|
|
verify=False, # TODO: REMOVE THIS NOTHALAL!!!!!
|
2024-11-02 23:15:51 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
if response.status_code != 200:
|
2024-11-11 08:41:30 +00:00
|
|
|
raise KanidmQueryError(
|
2024-11-11 16:36:16 +00:00
|
|
|
f"Kanidm returned {response.status_code} unexpected HTTP status code. Endpoint: {full_endpoint}. Error: {response.text}."
|
2024-11-02 23:15:51 +00:00
|
|
|
)
|
2024-11-11 17:09:14 +00:00
|
|
|
return response.json()
|
2024-11-11 16:07:34 +00:00
|
|
|
|
2024-11-02 23:15:51 +00:00
|
|
|
except Exception as error:
|
2024-11-11 16:07:34 +00:00
|
|
|
raise KanidmQueryError(f"Kanidm request failed! Error: {str(error)}")
|
2024-11-02 23:15:51 +00:00
|
|
|
|
|
|
|
@staticmethod
|
2024-11-14 23:30:24 +00:00
|
|
|
def create_user(
|
|
|
|
username: str,
|
2024-11-15 14:30:51 +00:00
|
|
|
password: Optional[str] = None, # TODO legacy?
|
2024-11-14 23:30:24 +00:00
|
|
|
displayname: Optional[str] = None,
|
|
|
|
email: Optional[str] = None,
|
|
|
|
directmemberof: Optional[list[str]] = None,
|
|
|
|
memberof: Optional[list[str]] = None,
|
|
|
|
) -> None:
|
2024-11-11 00:33:17 +00:00
|
|
|
data = {
|
|
|
|
"attrs": {
|
|
|
|
"name": [username],
|
2024-11-14 23:30:24 +00:00
|
|
|
"displayname": [displayname if displayname else username],
|
|
|
|
"mail": [email if email else f"{username}@{get_domain()}"],
|
|
|
|
"class": ["user"], # TODO read more about it
|
2024-11-11 00:33:17 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-11-14 23:30:24 +00:00
|
|
|
if directmemberof:
|
|
|
|
data["attrs"]["directmemberof"] = directmemberof
|
|
|
|
if memberof:
|
|
|
|
data["attrs"]["memberof"] = memberof
|
|
|
|
|
2024-11-02 23:15:51 +00:00
|
|
|
return KanidmUserRepository._send_query(
|
2024-11-11 00:33:17 +00:00
|
|
|
endpoint="person",
|
|
|
|
method="POST",
|
|
|
|
data=data,
|
2024-11-02 23:15:51 +00:00
|
|
|
)
|
|
|
|
|
2024-10-28 21:57:23 +00:00
|
|
|
def get_users(
|
|
|
|
exclude_primary: bool = False,
|
|
|
|
exclude_root: bool = False,
|
|
|
|
) -> list[UserDataUser]:
|
2024-11-11 17:41:44 +00:00
|
|
|
users_data = KanidmUserRepository._send_query(endpoint="person", method="GET")
|
2024-11-11 17:37:17 +00:00
|
|
|
users = []
|
|
|
|
for user in users_data:
|
|
|
|
attrs = user.get("attrs", {})
|
|
|
|
user_type = UserDataUser(
|
|
|
|
uuid=attrs.get("uuid", [None])[0],
|
2024-11-11 17:49:51 +00:00
|
|
|
username=attrs.get("name", [None])[0],
|
2024-11-14 23:30:24 +00:00
|
|
|
ssh_keys=["test"], # TODO: подключить реальные SSH-ключи
|
2024-11-11 17:37:17 +00:00
|
|
|
displayname=attrs.get("displayname", [None])[0],
|
|
|
|
email=attrs.get("mail", [None])[0],
|
|
|
|
origin=UserDataUserOrigin.NORMAL, # TODO
|
2024-11-14 23:30:24 +00:00
|
|
|
directmemberof=attrs.get("directmemberof", []),
|
|
|
|
memberof=attrs.get("memberof", []),
|
2024-11-11 17:37:17 +00:00
|
|
|
)
|
|
|
|
users.append(user_type)
|
|
|
|
return users
|
2024-10-28 21:57:23 +00:00
|
|
|
|
|
|
|
def delete_user(username: str) -> None:
|
|
|
|
"""Deletes an existing user"""
|
2024-11-14 23:30:24 +00:00
|
|
|
return KanidmUserRepository._send_query(
|
|
|
|
endpoint=f"person/{username}", method="DELETE"
|
|
|
|
)
|
2024-10-28 21:57:23 +00:00
|
|
|
|
2024-11-14 23:30:24 +00:00
|
|
|
def update_user(
|
|
|
|
username: str,
|
2024-11-15 14:30:51 +00:00
|
|
|
password: Optional[str] = None, # TODO legacy?
|
2024-11-14 23:30:24 +00:00
|
|
|
displayname: Optional[str] = None,
|
|
|
|
email: Optional[str] = None,
|
|
|
|
directmemberof: Optional[list[str]] = None,
|
|
|
|
memberof: Optional[list[str]] = None,
|
|
|
|
) -> None:
|
2024-10-28 21:57:23 +00:00
|
|
|
"""Updates the password of an existing user"""
|
2024-11-14 23:30:24 +00:00
|
|
|
|
|
|
|
data = {
|
|
|
|
"attrs": {
|
|
|
|
"displayname": [displayname if displayname else username],
|
|
|
|
"mail": [email if email else f"{username}@{get_domain()}"],
|
|
|
|
"class": ["user"], # TODO read more about it
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if directmemberof:
|
|
|
|
data["attrs"]["directmemberof"] = directmemberof
|
|
|
|
if memberof:
|
|
|
|
data["attrs"]["memberof"] = memberof
|
|
|
|
|
|
|
|
return KanidmUserRepository._send_query(
|
|
|
|
endpoint=f"person/{username}",
|
|
|
|
method="PATCH",
|
|
|
|
data=data,
|
|
|
|
)
|
2024-10-28 21:57:23 +00:00
|
|
|
|
|
|
|
def get_user_by_username(username: str) -> Optional[UserDataUser]:
|
|
|
|
"""Retrieves user data (UserDataUser) by username"""
|
2024-11-14 23:30:24 +00:00
|
|
|
user_data = KanidmUserRepository._send_query(
|
|
|
|
endpoint=f"person/{username}",
|
|
|
|
method="GET",
|
|
|
|
)
|
|
|
|
|
|
|
|
if not user_data or "attrs" not in user_data:
|
|
|
|
return None
|
|
|
|
|
|
|
|
attrs = user_data["attrs"]
|
|
|
|
|
|
|
|
return UserDataUser(
|
|
|
|
uuid=attrs.get("uuid", [None])[0],
|
|
|
|
username=attrs.get("name", [None])[0],
|
|
|
|
displayname=attrs.get("displayname", [None])[0],
|
|
|
|
email=attrs.get("mail", [None])[0],
|
|
|
|
ssh_keys=attrs.get("ssh_keys", []),
|
|
|
|
origin=UserDataUserOrigin.NORMAL, # TODO
|
|
|
|
directmemberof=attrs.get("directmemberof", []),
|
|
|
|
memberof=attrs.get("memberof", []),
|
|
|
|
)
|