2022-08-26 19:08:52 +07:00
|
|
|
#!/usr/bin/env python
|
2022-07-18 03:48:15 +07:00
|
|
|
|
|
|
|
"""
|
|
|
|
Quick and dirty script for things that I can't/don't have time to do properly yet
|
|
|
|
TODO: retire this script
|
|
|
|
"""
|
|
|
|
|
2022-07-23 23:59:29 +07:00
|
|
|
import base64
|
2022-07-18 03:48:15 +07:00
|
|
|
import json
|
2024-01-06 01:25:55 +07:00
|
|
|
import json
|
|
|
|
import pexpect
|
2022-07-18 03:48:15 +07:00
|
|
|
import requests
|
2024-01-06 01:25:55 +07:00
|
|
|
import subprocess
|
2022-07-23 23:59:29 +07:00
|
|
|
import sys
|
2023-02-22 18:33:48 +07:00
|
|
|
import urllib
|
2022-07-18 03:48:15 +07:00
|
|
|
|
2022-07-23 23:59:29 +07:00
|
|
|
from rich.console import Console
|
|
|
|
from kubernetes import client, config
|
2024-01-06 01:25:55 +07:00
|
|
|
from kubernetes.stream import stream
|
2022-07-18 03:48:15 +07:00
|
|
|
|
|
|
|
# https://git.khuedoan.com/user/settings/applications
|
|
|
|
# Doing this properly inside the cluster requires:
|
|
|
|
# - Kubernetes service account
|
2024-01-16 14:15:39 +07:00
|
|
|
config.load_config()
|
2022-07-18 03:48:15 +07:00
|
|
|
|
2022-07-23 23:59:29 +07:00
|
|
|
gitea_host = client.NetworkingV1Api().read_namespaced_ingress('gitea', 'gitea').spec.rules[0].host
|
2023-11-26 02:35:19 +07:00
|
|
|
gitea_user_secret = client.CoreV1Api().read_namespaced_secret('gitea-admin-secret', 'gitea')
|
|
|
|
gitea_user = base64.b64decode(gitea_user_secret.data['username']).decode("utf-8")
|
|
|
|
gitea_pass = base64.b64decode(gitea_user_secret.data['password']).decode("utf-8")
|
2024-03-02 23:34:46 +07:00
|
|
|
gitea_url = f"http://{gitea_host}"
|
2022-07-18 03:48:15 +07:00
|
|
|
|
2024-01-06 01:25:55 +07:00
|
|
|
kanidm_host = client.NetworkingV1Api().read_namespaced_ingress('kanidm', 'kanidm').spec.rules[0].host
|
|
|
|
|
2024-01-21 16:47:40 +07:00
|
|
|
def apply_secret(name: str, namespace: str, data: dict) -> None:
|
2023-11-26 02:35:19 +07:00
|
|
|
try:
|
|
|
|
client.CoreV1Api().read_namespaced_secret(name, namespace)
|
2024-01-21 16:47:40 +07:00
|
|
|
patch_body = client.V1Secret(
|
|
|
|
metadata=client.V1ObjectMeta(name=name),
|
|
|
|
data=data,
|
|
|
|
)
|
|
|
|
client.CoreV1Api().replace_namespaced_secret(name, namespace, patch_body)
|
2023-11-26 02:35:19 +07:00
|
|
|
except client.exceptions.ApiException:
|
|
|
|
# Secret doesn't exist, create a new one
|
|
|
|
new_secret = client.V1Secret(
|
|
|
|
metadata=client.V1ObjectMeta(name=name),
|
|
|
|
data=data,
|
|
|
|
)
|
|
|
|
client.CoreV1Api().create_namespaced_secret(namespace, new_secret)
|
2022-07-18 03:48:15 +07:00
|
|
|
|
2024-01-09 00:28:48 +07:00
|
|
|
def setup_gitea_access_token(name: str, scopes: list[str]) -> None:
|
2022-07-18 03:48:15 +07:00
|
|
|
current_tokens = requests.get(
|
|
|
|
url=f"{gitea_url}/api/v1/users/{gitea_user}/tokens",
|
2024-03-02 23:34:46 +07:00
|
|
|
auth=(gitea_user,gitea_pass),
|
2022-07-18 03:48:15 +07:00
|
|
|
).json()
|
|
|
|
|
|
|
|
if not any(token['name'] == name for token in current_tokens):
|
|
|
|
resp = requests.post(
|
|
|
|
url=f"{gitea_url}/api/v1/users/{gitea_user}/tokens",
|
2024-03-02 23:34:46 +07:00
|
|
|
auth=(gitea_user,gitea_pass),
|
2022-07-18 03:48:15 +07:00
|
|
|
headers={
|
|
|
|
'Content-Type': 'application/json'
|
|
|
|
},
|
|
|
|
data=json.dumps({
|
2024-01-09 00:28:48 +07:00
|
|
|
'name': name,
|
|
|
|
'scopes': scopes
|
2022-07-18 03:48:15 +07:00
|
|
|
})
|
|
|
|
)
|
|
|
|
|
|
|
|
if resp.status_code == 201:
|
2024-01-21 16:47:40 +07:00
|
|
|
apply_secret(
|
2023-11-26 02:09:21 +07:00
|
|
|
f"gitea.{name}",
|
|
|
|
"global-secrets",
|
2022-07-18 03:48:15 +07:00
|
|
|
{
|
2023-11-26 02:35:19 +07:00
|
|
|
'token': base64.b64encode(resp.json()['sha1'].encode("utf-8")).decode("utf-8")
|
2022-07-18 03:48:15 +07:00
|
|
|
}
|
|
|
|
)
|
|
|
|
else:
|
|
|
|
print(f"Error creating access token {name} ({resp.status_code})")
|
|
|
|
print(resp.content)
|
|
|
|
sys.exit(1)
|
|
|
|
|
|
|
|
def setup_gitea_oauth_app(name: str, redirect_uri: str) -> None:
|
2024-01-08 10:56:44 +07:00
|
|
|
# TODO use the new global application, while it's there in the UI, there's no API yet.
|
2022-07-18 03:48:15 +07:00
|
|
|
current_apps = requests.get(
|
|
|
|
url=f"{gitea_url}/api/v1/user/applications/oauth2",
|
2024-03-02 23:34:46 +07:00
|
|
|
auth=(gitea_user,gitea_pass),
|
2022-07-18 03:48:15 +07:00
|
|
|
).json()
|
|
|
|
|
|
|
|
if not any(app['name'] == name for app in current_apps):
|
|
|
|
resp = requests.post(
|
|
|
|
url=f"{gitea_url}/api/v1/user/applications/oauth2",
|
2024-03-02 23:34:46 +07:00
|
|
|
auth=(gitea_user,gitea_pass),
|
2022-07-18 03:48:15 +07:00
|
|
|
headers={
|
|
|
|
'Content-Type': 'application/json'
|
|
|
|
},
|
|
|
|
data=json.dumps({
|
|
|
|
'name': name,
|
2024-01-08 10:56:44 +07:00
|
|
|
'redirect_uris': [redirect_uri],
|
|
|
|
'confidential_client': True
|
2022-07-18 03:48:15 +07:00
|
|
|
})
|
|
|
|
)
|
|
|
|
|
|
|
|
if resp.status_code == 201:
|
2024-01-21 16:47:40 +07:00
|
|
|
apply_secret(
|
2023-11-26 02:09:21 +07:00
|
|
|
f"gitea.{name}",
|
|
|
|
"global-secrets",
|
2022-07-18 03:48:15 +07:00
|
|
|
{
|
2023-11-26 02:35:19 +07:00
|
|
|
'client_id': base64.b64encode(resp.json()['client_id'].encode("utf-8")).decode("utf-8"),
|
|
|
|
'client_secret': base64.b64encode(resp.json()['client_secret'].encode("utf-8")).decode("utf-8"),
|
2022-07-18 03:48:15 +07:00
|
|
|
}
|
|
|
|
)
|
|
|
|
else:
|
|
|
|
print(f"Error creating OAuth application {name} ({resp.status_code})")
|
|
|
|
print(resp.content)
|
|
|
|
sys.exit(1)
|
|
|
|
|
2024-01-17 01:08:06 +07:00
|
|
|
def setup_gitea_auth_with_dex():
|
|
|
|
gitea_pod = client.CoreV1Api().list_namespaced_pod(namespace='gitea', label_selector='app=gitea').items[0].metadata.name
|
|
|
|
client_secret = base64.b64decode(
|
|
|
|
client.CoreV1Api().read_namespaced_secret('dex.gitea', 'global-secrets').data['client_secret']
|
|
|
|
).decode("utf-8")
|
|
|
|
discovery_url = f"https://{client.NetworkingV1Api().read_namespaced_ingress('dex', 'dex').spec.rules[0].host}/.well-known/openid-configuration"
|
|
|
|
|
|
|
|
# TODO currently there's no API to add new authentication sources in Gitea,
|
|
|
|
# so we have to workaround by running Gitea CLI in a Gitea pod.
|
|
|
|
stream(
|
|
|
|
client.CoreV1Api().connect_get_namespaced_pod_exec,
|
|
|
|
gitea_pod,
|
|
|
|
'gitea',
|
|
|
|
command=[
|
|
|
|
'gitea', 'admin', 'auth', 'add-oauth',
|
|
|
|
'--name', 'Dex',
|
|
|
|
'--provider', 'openidConnect',
|
|
|
|
'--key', 'gitea',
|
|
|
|
'--secret', client_secret,
|
|
|
|
'--auto-discover-url', discovery_url
|
|
|
|
],
|
|
|
|
stderr=True, stdin=False,
|
|
|
|
stdout=False, tty=False
|
|
|
|
)
|
|
|
|
|
2024-01-06 01:25:55 +07:00
|
|
|
def reset_kanidm_account_password(account: str) -> str:
|
|
|
|
resp = stream(
|
|
|
|
client.CoreV1Api().connect_get_namespaced_pod_exec,
|
|
|
|
'kanidm-0',
|
|
|
|
'kanidm',
|
|
|
|
command=["kanidmd", "recover-account", "--output", "json", account],
|
2024-04-18 17:24:28 +07:00
|
|
|
stderr=False, stdin=False,
|
|
|
|
stdout=True, tty=False
|
|
|
|
).splitlines()[-1]
|
2024-01-06 01:25:55 +07:00
|
|
|
|
|
|
|
return json.loads(resp)['password']
|
|
|
|
|
|
|
|
# TODO Proper automation will be added later, waiting for client library update:
|
|
|
|
# https://github.com/kanidm/kanidm/pull/2301
|
|
|
|
def kanidm_login(accounts: list[str]) -> None:
|
|
|
|
for account in accounts:
|
|
|
|
password = reset_kanidm_account_password(account)
|
|
|
|
|
|
|
|
# There's no way to input password using the standard library, so we have to use pexpect
|
|
|
|
# https://stackoverflow.com/questions/2387731/use-subprocess-to-send-a-password
|
|
|
|
cli_login = pexpect.spawn(f"kanidm login --url https://{kanidm_host} --name {account}")
|
|
|
|
cli_login.sendline(password)
|
|
|
|
cli_login.read()
|
|
|
|
|
|
|
|
def setup_kanidm_group(name: str) -> None:
|
|
|
|
subprocess.run(
|
|
|
|
["kanidm", "group", "create", "--url", f"https://{kanidm_host}", "--name", "idm_admin", name],
|
|
|
|
capture_output=True,
|
|
|
|
)
|
|
|
|
|
|
|
|
def setup_kanidm_oauth_app(name: str, redirect_uri: str) -> None:
|
|
|
|
try:
|
|
|
|
subprocess.run(
|
2024-09-02 12:45:30 +07:00
|
|
|
["kanidm", "system", "oauth2", "create", "--url", f"https://{kanidm_host}", "--name", "idm_admin", name, name, redirect_uri],
|
2024-01-06 01:25:55 +07:00
|
|
|
capture_output=True,
|
|
|
|
check=True,
|
|
|
|
)
|
|
|
|
except subprocess.CalledProcessError:
|
|
|
|
return
|
|
|
|
|
|
|
|
# TODO https://github.com/dexidp/dex/pull/3188
|
|
|
|
subprocess.run(
|
2024-09-02 12:45:30 +07:00
|
|
|
["kanidm", "system", "oauth2", "warning-insecure-client-disable-pkce", "--url", f"https://{kanidm_host}", "--name", "idm_admin", name],
|
2024-01-06 01:25:55 +07:00
|
|
|
capture_output=True,
|
|
|
|
check=True,
|
|
|
|
)
|
|
|
|
|
|
|
|
subprocess.run(
|
|
|
|
# TODO better group management
|
2024-09-02 12:45:30 +07:00
|
|
|
["kanidm", "system", "oauth2", "create-scope-map", "--url", f"https://{kanidm_host}", "--name", "idm_admin", name, "editor", "openid", "profile", "email", "groups"],
|
2024-01-06 01:25:55 +07:00
|
|
|
capture_output=True,
|
|
|
|
check=True,
|
|
|
|
)
|
|
|
|
|
|
|
|
client_secret = json.loads(subprocess.run(
|
2024-09-02 12:45:30 +07:00
|
|
|
["kanidm", "system", "oauth2", "show-basic-secret", "--url", f"https://{kanidm_host}", "--name", "idm_admin", "--output", "json", name],
|
2024-01-06 01:25:55 +07:00
|
|
|
capture_output=True,
|
|
|
|
check=True,
|
|
|
|
).stdout.decode("utf-8"))['secret']
|
|
|
|
|
2024-01-21 16:47:40 +07:00
|
|
|
apply_secret(
|
2024-01-06 01:25:55 +07:00
|
|
|
f"kanidm.{name}",
|
|
|
|
"global-secrets",
|
|
|
|
{
|
|
|
|
'client_id': base64.b64encode(name.encode("utf-8")).decode("utf-8"),
|
|
|
|
'client_secret': base64.b64encode(client_secret.encode("utf-8")).decode("utf-8"),
|
|
|
|
}
|
|
|
|
)
|
|
|
|
|
2022-07-18 03:48:15 +07:00
|
|
|
def main() -> None:
|
|
|
|
with Console().status("Completing the remaining sorcery"):
|
|
|
|
gitea_access_tokens = [
|
2024-01-09 00:28:48 +07:00
|
|
|
{
|
|
|
|
'name': 'renovate',
|
|
|
|
'scopes': [
|
|
|
|
"write:repository",
|
|
|
|
"read:user",
|
|
|
|
"write:issue",
|
|
|
|
"read:organization",
|
|
|
|
"read:misc"
|
|
|
|
]
|
|
|
|
}
|
2022-07-18 03:48:15 +07:00
|
|
|
]
|
|
|
|
|
|
|
|
gitea_oauth_apps = [
|
2024-01-08 10:56:44 +07:00
|
|
|
{'name': 'woodpecker', 'redirect_uri': f"https://{client.NetworkingV1Api().read_namespaced_ingress('woodpecker-server', 'woodpecker').spec.rules[0].host}/authorize"},
|
2022-07-18 03:48:15 +07:00
|
|
|
]
|
|
|
|
|
2024-01-06 01:25:55 +07:00
|
|
|
kanidm_groups = [
|
|
|
|
# TODO better group management
|
|
|
|
{'name': 'editor'},
|
|
|
|
]
|
|
|
|
|
|
|
|
kanidm_oauth_apps = [
|
|
|
|
{'name': 'dex', 'redirect_uri': f"https://{client.NetworkingV1Api().read_namespaced_ingress('dex', 'dex').spec.rules[0].host}/callback"},
|
|
|
|
]
|
|
|
|
|
2024-01-09 00:28:48 +07:00
|
|
|
for token in gitea_access_tokens:
|
|
|
|
setup_gitea_access_token(token['name'], token['scopes'])
|
2022-07-18 03:48:15 +07:00
|
|
|
|
|
|
|
for app in gitea_oauth_apps:
|
|
|
|
setup_gitea_oauth_app(app['name'], app['redirect_uri'])
|
|
|
|
|
2024-01-17 01:08:06 +07:00
|
|
|
setup_gitea_auth_with_dex()
|
|
|
|
|
2024-01-06 01:25:55 +07:00
|
|
|
kanidm_login(["admin", "idm_admin"])
|
|
|
|
|
|
|
|
for group in kanidm_groups:
|
|
|
|
setup_kanidm_group(group['name'])
|
|
|
|
|
|
|
|
for app in kanidm_oauth_apps:
|
|
|
|
setup_kanidm_oauth_app(app['name'], app['redirect_uri'])
|
|
|
|
|
2022-07-18 03:48:15 +07:00
|
|
|
if __name__ == '__main__':
|
|
|
|
main()
|