feat: add k8s/argo-apps/authelia #756
8 changed files with 465 additions and 1 deletions
feat: add k8s/argo-apps/authelia
- add scripts/users.py to manage users against lldap - add smtp values for lldap so it can send emails
commit
c9fcd56bda
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -21,3 +21,4 @@ AGENTS.md
|
|||
k8s/charts/README.md
|
||||
.opencode/
|
||||
opencode.json
|
||||
*.pyc
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ yaml-files:
|
|||
|
||||
ignore: |
|
||||
k8s/charts/
|
||||
**/sealedsecrets.yaml
|
||||
|
||||
|
||||
extends: default
|
||||
|
|
|
|||
|
|
@ -13,3 +13,9 @@ ethtool -K eno1 tx off rx off
|
|||
```bash
|
||||
helm upgrade argo-cd argo/argo-cd -n argo-cd -f k8s/helm/argo-cd/values.yaml
|
||||
```
|
||||
|
||||
- create a new user and send them an email
|
||||
|
||||
```bash
|
||||
scripts/users.py add -u catalin -n cătălin -e catalin@roboces.dev
|
||||
```
|
||||
|
|
|
|||
128
k8s/argo-apps/authelia.yaml
Normal file
128
k8s/argo-apps/authelia.yaml
Normal file
|
|
@ -0,0 +1,128 @@
|
|||
---
|
||||
apiVersion: argoproj.io/v1alpha1
|
||||
kind: Application
|
||||
metadata:
|
||||
name: authelia
|
||||
namespace: argo-cd
|
||||
spec:
|
||||
destination:
|
||||
name: ''
|
||||
namespace: apps-roboces
|
||||
server: https://kubernetes.default.svc
|
||||
sources:
|
||||
- chart: authelia
|
||||
repoURL: https://charts.authelia.com
|
||||
targetRevision: 0.11.6
|
||||
helm:
|
||||
valuesObject:
|
||||
|
||||
enabled: true
|
||||
pod:
|
||||
autoscaling:
|
||||
enabled: true
|
||||
kind: Deployment
|
||||
resources:
|
||||
requests:
|
||||
cpu: 100m
|
||||
memory: 128Mi
|
||||
limits:
|
||||
cpu: 1
|
||||
memory: 1024Mi
|
||||
service:
|
||||
type: LoadBalancer
|
||||
ingress:
|
||||
enabled: true
|
||||
className: traefik
|
||||
annotations:
|
||||
traefik.ingress.kubernetes.io/router.entrypoints: websecure
|
||||
traefik.ingress.kubernetes.io/router.tls: "true"
|
||||
secret:
|
||||
existingSecret: secrets-authelia
|
||||
mountPath: /secrets
|
||||
additionalSecrets:
|
||||
secrets-authelia:
|
||||
path: secrets-authelia
|
||||
items:
|
||||
- key: authentication.ldap.password.txt
|
||||
path: authentication.ldap.password.txt
|
||||
- key: identity_validation.reset_password.jwt.hmac.key
|
||||
path: identity_validation.reset_password.jwt.hmac.key
|
||||
- key: session.encryption.key
|
||||
path: session.encryption.key
|
||||
- key: smtp_password.txt
|
||||
path: smtp_password.txt
|
||||
- key: storage.encryption.key
|
||||
path: storage.encryption.key
|
||||
persistence:
|
||||
enabled: true
|
||||
existingClaim: authelia-data
|
||||
configMap:
|
||||
log:
|
||||
level: info
|
||||
session:
|
||||
name: authelia_session
|
||||
same_site: lax
|
||||
expiration: 1 hour
|
||||
inactivity: 5 minutes
|
||||
remember_me: 1 month
|
||||
cookies:
|
||||
- domain: roboces.dev
|
||||
subdomain: auth
|
||||
- domain: auth.fuku
|
||||
subdomain: ''
|
||||
authentication_backend:
|
||||
ldap:
|
||||
enabled: true
|
||||
implementation: lldap
|
||||
address: ldap://lldap.apps-fuku.svc.cluster.local:3890
|
||||
base_dn: dc=fuku,dc=local
|
||||
additional_users_dn: ou=people
|
||||
users_filter: (&({username_attribute}={input})(objectClass=person))
|
||||
additional_groups_dn: ou=groups
|
||||
groups_filter: (member={dn})
|
||||
user: uid=admin,ou=people,dc=fuku,dc=local
|
||||
attributes:
|
||||
distinguished_name: dn
|
||||
username: uid
|
||||
display_name: cn
|
||||
mail: mail
|
||||
member_of: memberOf
|
||||
group_name: cn
|
||||
password:
|
||||
disabled: false
|
||||
secret_name: secrets-authelia
|
||||
path: authentication.ldap.password.txt
|
||||
storage:
|
||||
local:
|
||||
enabled: true
|
||||
path: /config/db.sqlite3
|
||||
notifier:
|
||||
smtp:
|
||||
enabled: true
|
||||
address: submissions://mail.fukurokuju.dev:465
|
||||
sender: Authelia <auth@fukurokuju.dev>
|
||||
subject: "Your verification code is {code}"
|
||||
identifier: auth.fuku
|
||||
startup_check_address: test@authelia.com
|
||||
disable_html_emails: false
|
||||
disable_require_tls: false
|
||||
disable_starttls: false
|
||||
username: auth@fukurokuju.dev
|
||||
password:
|
||||
disabled: false
|
||||
secret_name: secrets-authelia
|
||||
path: smtp_password.txt
|
||||
tls:
|
||||
server_name: mail.fukurokuju.dev
|
||||
skip_verify: false
|
||||
minimum_version: TLS1.2
|
||||
access_control:
|
||||
default_policy: two_factor
|
||||
- repoURL: https://git.roboces.dev/catalin/fukuops.git
|
||||
path: k8s/services/authelia
|
||||
targetRevision: feat/authelia
|
||||
project: roboces
|
||||
syncPolicy:
|
||||
automated:
|
||||
prune: true
|
||||
selfHeal: true
|
||||
13
k8s/services/authelia/pvc.yaml
Normal file
13
k8s/services/authelia/pvc.yaml
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
---
|
||||
apiVersion: v1
|
||||
kind: PersistentVolumeClaim
|
||||
metadata:
|
||||
name: authelia-data
|
||||
namespace: apps-roboces
|
||||
spec:
|
||||
accessModes:
|
||||
- ReadWriteMany
|
||||
storageClassName: truenas-nfs-csi
|
||||
resources:
|
||||
requests:
|
||||
storage: 1Gi
|
||||
17
k8s/services/authelia/sealedsecrets.yaml
Normal file
17
k8s/services/authelia/sealedsecrets.yaml
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
apiVersion: bitnami.com/v1alpha1
|
||||
kind: SealedSecret
|
||||
metadata:
|
||||
name: secrets-authelia
|
||||
namespace: apps-roboces
|
||||
spec:
|
||||
encryptedData:
|
||||
authentication.ldap.password.txt: AgBwsqdgEOQ8r/yUOD82Cwr44jxqV9RPDTzMYju8IfWx7b4iDKtZVidLK4RxBCp1+MPyWmLs7H4jec3Xsr10da/V+LaS1Elib+Sp2CkFZPGj1/XAcu5w3k/BnMcsRWJEJtzU251NVon6IVEzszcGOaVsPqf6aIvnKgOJEmp1boMlnemkvtaeXVS0TuSLXCQEMDfGMh9X8D9je315Sdqpczo8YLz6jyj6RxlgXeygRXkXANd2KFOyfLNUAnzm4Tl6PMjULnXvsxLhlc35+am6IsZGVLcdRMPY12UpHeZyVIgJBZSdo/gLTJS/hLhL2sPJv8HksL6YfvDnIWqu3PxVwI3BGtW2znBQaEIFXtOIXL9EDG0NMAZSBYCRpjrPNHP7xj7ruqjg4eU1iUj6yJpIv9322L/qhFWViK9wXc3+0L2n2q4qVBgrZxcacgqtHrCYj5Uo9iluVPWwagcKFYRXBupqaUUTso7LtSsv4WbOrJzYzE9mZk+feH1ZNvBp13JfWNdRiso5rpl0tZFJ7oW4G/CJuWg2MmL3UNViYB/ntwFWZWT8dxCEq2LI3UxeawHntzvk82kFyZduLUwhLvY4WfQB4yb//gBOpc0OyHa+djwECIyy3Jh+70LG+1YeVc8yQm52eu9+mzRSwDlqspKZYfwbMPCCiQj6AiSfg9Ho5+ITq1PgEt3oo9b0JkYFmEAgT88Y6z0DRQrM9YOXCfazMg==
|
||||
identity_validation.reset_password.jwt.hmac.key: AgAQRknTOoC5tbCitLZkRtcIs3VOaubEvlhRmGtHh/OS0v6lgU2Gcl5C6jisu/+MqQba4euzMACLVgSMiEkERY2GrMijecfVf5LOkKKwQ0Xjiw4a+MlSVs5KR1Ovlz414//3GfUCttunnDRnH6xQbsQjWLCI6lv6Umb40TQHOnRmb83wmSkBa/P9WR35EQ6IcES3q7Ad4ZpNtLaavEnB8NuLZg3yaJpN+1yuZPeMgTPUtTG/e9FLdBs3S8AYFIcrUnDLViUo9DfXumMs1DlTsmABpjyIgjIeywOmCxMYZYvQCJrRGYlKZRdoCoafhAPxt67hdUYOe56cq3kqlTNTdURMbYup7cIkW2hQ8ZiyPvYk++RIhV/cPWOSjh6CLxMq26Si1c2Lg3aAVFxQcDiXbdprjHzbKzFfKO/5LVnhT7O651BqkCguPC2vVoaeuxblHjqouBD7ZmPazIQ9J/uDUmvm5UPjEN+Ln+enrLXdoJN9HMor410/PJ+9xPAHXhdrXRS6j5TlN5q1pz0gj2Qmnv3cunsyLo+ZJ97F3EkSYPVoK3sMXGn2fdKy/lc9vPMMvtH/tLdhWefxd25APahStZHv4Mb7O6Z+A4uFiP9Qao64jeLj08LWBbJPefT7Y+KthugN6cW4y/xQN4UFvpPHhGB36yb+45eIBzOJHltOqZ+wldjUHeBgMdkODAQYYbCzhDuVVm2iMGQqUi/Vk+EayaV70prcAELgRskseME2MwfrhYMAKS91tfMbL2dGTQ==
|
||||
session.encryption.key: AgCjw3K7solE1INH+VIwEeP2U70HCaY25Iu6sq5C7j8nuW8JzbSCBPeY+qTS76l4sIyLlLekk7ke5kCPpZo+Jsz62UQJMGaSASRy0FKQIxgn5UIkObB+qziGjC8WNj6y/JmbvPNiIp+aw0Nd3OmVLRkvIjJDkyMf/iW1oGeLctwRop9Y+wi7lDPyEHH+CaNPCLYidhAJFG785qibqY8jTxtHsEts2T+hSXz75v5abYS+lTFtFV7RDWudkWG6dPjUKwYGw7l/yQm5p2qzzvhoyCrQItdNuEvGrZQsnXZlq4ik0C7t977uiU+fumYKFQvHzVuQTMjjo9G5AlhUG/4w2olMqOMZoJ0u+hHFBX1K1uYCUr1hd2343jd+y86iObZzTazlTGdP142lXk9+CT+akpQIVX0Mk5mwtemCYG+64ebSQm+SVB9Ffgxx3Ips6BOhqwIO0fLB1iz4BFVUp+87juyHZOP/5uC+KeV3+DHaQDrHRo6DxF5D5CRXBCcHXeWBLkDXSENgD4kvRee7fEQf60ajvi9VFTPjk86KXCbOZVlZPPrqWG7Dh+kaYOjWCQXQvFzYAq6/Dr5ZDRPJ/X989zHjPFFGRQ7Ol9uyjT/7I/2IBfdQB1Ep5BWty50PBVIqbYKRzQN0g0x5BAj5/2J9mOX/0BNGExVCNn53rfc8yDurfP0q1nbYk1t+fv0T9oI7RTPYIVWtUnshG8fwYK1PARNYBbKrGwjWtTdVxzV+ocunmmOHQhvOFrTnsfupGQ==
|
||||
smtp_password.txt: AgB8pRbQxScSBRty04Z5oen7F52Y7PnwF9gJ6fdbUTRobE7j2fWiHMQk4l1FTztW3Muz3B/MQZBgBEzulpkDJ1TrvTe5I7JFdANr3lmlKjy0CG2NcQ7Vma7Bqu2SVnL8wxi/3IBTGxVa6bplGrkUCsIr/qWLhkVknOpAaUvOUuECT8PVxUZq/s4UMddkEf4N3IQW7j/60xwzomMp4e5eZ8YTH0/ZGcuw9JOQSOoJDzvJ5B+wZF7q7j9LQc4gBPHQiFFMVlZrhHA0fdjfPxbb7QEiGdMBxeZ0JqaaFBnr4FG7hRY5/H9RS9iZCFH9QY6X86ul9ZgiJYVyrvZFKSBsW+THlLZ7MACLvOs/zdXhpc2SthccoK8ykGC/inWTnfodQpH7N4zJq7akXxFy9/em4ssjWul4coeLNc+xhyi5VauLQQfRpgijc1kVJeQbrUrMe+aE1tlRdgRx76OKiFym1t47+zN3eqRf+NegNBGPSdAtKEBKfNPrcFQmlZVR6fRZqiBxG0wE90nfq9fuw3RDnVU/0LsuxwTz99gWMWl8ySuo+EAhf+JmB26/Pi1hnByeNeD8FsVkUiC6XnwAVC+ytnaVEfatrKmSJyzRhTSuFUoFxD5YV0aQgILks4+CpQwn300s0bqkLOP0etigZCCPq4cR+t+FFYAkuVjr+xPsoww0UOgokHsZKoXYi0n96UjbT0w+zoHhqQTBrPPWhPjDogZBKBCnth5eUtgrhSA2
|
||||
storage.encryption.key: AgBt8lTRtRqn+PiTNjgmJ97/rEqKEE70KZLuVtyvBM2ek70E555vx16BsCN3ATvwcr5dw+nAwYKpHJzS+mgTloLWmz6L2HqtxhlnaKC7elFeOza7tPKmKeKV3zXQ8YEA9eWjKdUN2TzO5s10iPCp4esUy5asO5DS/QVyPHMUfufu3iwHCDgnDEGlQ0pO1LMT2FwJ2QYX4Cub+OjUVrkBm8VbNdFqIw3OBtahRX2gx+o1d55psfi08ylEMKUwkHXYtkBheDvYL7SRv8xHKHGrN1loWHZOby1GOTzOnOpL6mjJoU+4nlvEzVwxzuPOHDq24cfey7engu0IfrZ+9Fs+icFjInY7cpowlNl0CXIWQm++ljtKVOUGUB2Bp6IwY1iwu3NVlwYVoZRCfSw5rLhccXnODYjXGMAwHF1cau4K8mkx1mD48dJnQE+y6AfrOPH+tCJ0dSDyuebNNlcE9HDmJseIGDP37Z3E/S9CCGprvLCdUYeOhXERyTevm5VJoboYq1/mhonPvY0bU4w3zVV3mXgr3Xi4SOupneUL+XfuQASRz1eGoQpAGxI8h4qWkgA66f8uPTh0hdM1JfLus9oXCtFsiYJzd+VtIZE+0JtI4ztGtm2JtgH80t2eiJTQolQihfpAXazg9fQSPH6NHsFcRplqC7emxdBZnI2F+TSjqcoX+RA78z+J2OP4ISs4ngF8USmm9/rJIRUq26XNLMjktSREBh/IwrpZWgiZqU/QzMhNriClH3n4vDHWJ03Xiw==
|
||||
template:
|
||||
metadata:
|
||||
name: secrets-authelia
|
||||
namespace: apps-roboces
|
||||
type: Opaque
|
||||
293
scripts/users.py
Executable file
293
scripts/users.py
Executable file
|
|
@ -0,0 +1,293 @@
|
|||
#!/usr/bin/env -S uv run --script
|
||||
# /// script
|
||||
# dependencies = ["ldap3", "typer", "httpx"]
|
||||
# ///
|
||||
|
||||
import base64
|
||||
import os
|
||||
import re
|
||||
import secrets
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
import httpx
|
||||
import typer
|
||||
from ldap3 import ALL, Connection, Server, SUBTREE
|
||||
|
||||
LLDAP_NAMESPACE = "apps-fuku"
|
||||
LLDAP_SECRET = "lldap-secrets"
|
||||
LLDAP_SERVICE = "lldap.apps-fuku.svc.cluster.local"
|
||||
LLDAP_LDAP_PORT = 3890
|
||||
LLDAP_BASE_DN = "dc=fuku,dc=local"
|
||||
LLDAP_ADMIN_USER = "uid=admin,ou=people,dc=fuku,dc=local"
|
||||
LLDAP_URL = "https://ldap.fukurokuju.dev"
|
||||
|
||||
app = typer.Typer(help="Manage Authelia users in the LLDAP backend.")
|
||||
|
||||
|
||||
def fail(msg: str) -> None:
|
||||
print(f"ERROR: {msg}", file=sys.stderr)
|
||||
raise typer.Exit(1)
|
||||
|
||||
|
||||
def run_kubectl(args: list[str]) -> subprocess.CompletedProcess[str]:
|
||||
return subprocess.run(
|
||||
["kubectl", *args],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=True,
|
||||
)
|
||||
|
||||
|
||||
def get_secret_value(key: str) -> str:
|
||||
result = run_kubectl(
|
||||
[
|
||||
"get",
|
||||
"secret",
|
||||
LLDAP_SECRET,
|
||||
"-n",
|
||||
LLDAP_NAMESPACE,
|
||||
"-o",
|
||||
f"jsonpath={{.data.{key}}}",
|
||||
]
|
||||
)
|
||||
return base64.b64decode(result.stdout).decode()
|
||||
|
||||
|
||||
def get_configmap_value(key: str) -> str:
|
||||
result = run_kubectl(
|
||||
[
|
||||
"get",
|
||||
"configmap",
|
||||
"lldap-config",
|
||||
"-n",
|
||||
LLDAP_NAMESPACE,
|
||||
"-o",
|
||||
f"jsonpath={{.data.{key}}}",
|
||||
]
|
||||
)
|
||||
return result.stdout.strip()
|
||||
|
||||
|
||||
def get_lldap_password() -> str:
|
||||
return get_secret_value("LLDAP_LDAP_USER_PASS")
|
||||
|
||||
|
||||
def get_lldap_hosts() -> list[str]:
|
||||
result = run_kubectl(
|
||||
[
|
||||
"get",
|
||||
"svc",
|
||||
"lldap",
|
||||
"-n",
|
||||
LLDAP_NAMESPACE,
|
||||
"-o",
|
||||
"jsonpath={.status.loadBalancer.ingress[*].ip}",
|
||||
]
|
||||
)
|
||||
ips = [ip.strip() for ip in result.stdout.split() if ip.strip()]
|
||||
if not ips:
|
||||
fail("could not discover lldap LoadBalancer IPs")
|
||||
return ips
|
||||
|
||||
|
||||
def lldap_connection() -> Connection:
|
||||
password = get_lldap_password()
|
||||
hosts = get_lldap_hosts()
|
||||
last_error = None
|
||||
for host in hosts:
|
||||
try:
|
||||
server = Server(host, port=LLDAP_LDAP_PORT, use_ssl=False, get_info=ALL)
|
||||
conn = Connection(
|
||||
server,
|
||||
user=LLDAP_ADMIN_USER,
|
||||
password=password,
|
||||
auto_bind=True,
|
||||
read_only=False,
|
||||
)
|
||||
return conn
|
||||
except Exception as e:
|
||||
last_error = e
|
||||
continue
|
||||
fail(f"could not connect to any lldap endpoint: {last_error}")
|
||||
|
||||
|
||||
def validate_username(username: str) -> None:
|
||||
if not re.match(r"^[a-zA-Z0-9_.-]+$", username):
|
||||
fail(f"invalid username: {username} (allowed: a-z, 0-9, _, ., -)")
|
||||
|
||||
|
||||
def trigger_password_reset(username: str) -> None:
|
||||
url = f"{LLDAP_URL}/auth/reset/step1/{username}"
|
||||
try:
|
||||
response = httpx.post(url, timeout=30.0)
|
||||
response.raise_for_status()
|
||||
except httpx.HTTPError as e:
|
||||
fail(f"failed to trigger password reset email: {e}")
|
||||
|
||||
print(f"Password reset email triggered for '{username}'.")
|
||||
|
||||
|
||||
def generate_random_password() -> str:
|
||||
return secrets.token_urlsafe(32)
|
||||
|
||||
|
||||
def get_group_id(conn: Connection, group_name: str) -> int | None:
|
||||
conn.search(
|
||||
f"ou=groups,{LLDAP_BASE_DN}",
|
||||
f"(cn={group_name})",
|
||||
SUBTREE,
|
||||
attributes=["uid"],
|
||||
)
|
||||
if not conn.entries:
|
||||
return None
|
||||
uid = conn.entries[0].uid.value
|
||||
try:
|
||||
return int(uid)
|
||||
except (TypeError, ValueError):
|
||||
return None
|
||||
|
||||
|
||||
def user_exists(conn: Connection, username: str) -> bool:
|
||||
conn.search(
|
||||
f"ou=people,{LLDAP_BASE_DN}",
|
||||
f"(uid={username})",
|
||||
SUBTREE,
|
||||
attributes=["uid"],
|
||||
)
|
||||
return bool(conn.entries)
|
||||
|
||||
|
||||
def email_in_use(conn: Connection, email: str) -> bool:
|
||||
conn.search(
|
||||
f"ou=people,{LLDAP_BASE_DN}",
|
||||
f"(mail={email})",
|
||||
SUBTREE,
|
||||
attributes=["uid"],
|
||||
)
|
||||
return bool(conn.entries)
|
||||
|
||||
|
||||
@app.command("list", help="List existing users ")
|
||||
def cmd_list() -> None:
|
||||
with lldap_connection() as conn:
|
||||
conn.search(
|
||||
f"ou=people,{LLDAP_BASE_DN}",
|
||||
"(objectClass=person)",
|
||||
SUBTREE,
|
||||
attributes=["uid", "cn", "mail", "memberOf"],
|
||||
)
|
||||
if not conn.entries:
|
||||
print("No users found.")
|
||||
return
|
||||
print(f"{'USERNAME':<20} {'DISPLAY NAME':<30} {'EMAIL':<30} GROUPS")
|
||||
for entry in conn.entries:
|
||||
uid = entry.uid.value if entry.uid else ""
|
||||
cn = entry.cn.value if entry.cn else ""
|
||||
mail = entry.mail.value if entry.mail else ""
|
||||
groups = ",".join(
|
||||
g.split(",")[0].replace("cn=", "") for g in entry.memberOf.values
|
||||
) if entry.memberOf else ""
|
||||
print(f"{uid:<20} {cn:<30} {mail:<30} {groups}")
|
||||
|
||||
|
||||
@app.command("add", help="Add a user. Omit --password to send an invite email")
|
||||
def cmd_add(
|
||||
username: str = typer.Option(..., "--username", "-u", help="Username"),
|
||||
displayname: str = typer.Option(..., "--displayname", "-n", help="Display name"),
|
||||
password: str | None = typer.Option(None, "--password", "-p", help="Password"),
|
||||
email: str = typer.Option("", "--email", "-e", help="Email address"),
|
||||
groups: list[str] = typer.Option(
|
||||
["users"], "--group", "-g", help="Group to add the user to (repeatable)"
|
||||
),
|
||||
) -> None:
|
||||
validate_username(username)
|
||||
|
||||
invite_mode = password is None
|
||||
if invite_mode and not email:
|
||||
fail("--email is required when --password is omitted")
|
||||
|
||||
effective_email = email or f"{username}@roboces.dev"
|
||||
|
||||
with lldap_connection() as conn:
|
||||
if user_exists(conn, username):
|
||||
fail(f"user '{username}' already exists")
|
||||
if email_in_use(conn, effective_email):
|
||||
fail(f"email '{effective_email}' is already in use")
|
||||
|
||||
if invite_mode:
|
||||
password = generate_random_password()
|
||||
|
||||
user_dn = f"uid={username},ou=people,{LLDAP_BASE_DN}"
|
||||
attrs = {
|
||||
"objectClass": ["inetOrgPerson", "posixAccount", "mailAccount", "person"],
|
||||
"uid": username,
|
||||
"cn": displayname,
|
||||
"mail": effective_email,
|
||||
}
|
||||
if not conn.add(user_dn, attributes=attrs):
|
||||
fail(f"failed to create user: {conn.result}")
|
||||
|
||||
if not conn.extend.standard.modify_password(user=user_dn, new_password=password):
|
||||
fail(f"failed to set password: {conn.result}")
|
||||
|
||||
for group_name in groups:
|
||||
group_id = get_group_id(conn, group_name)
|
||||
if group_id is None:
|
||||
print(f" WARNING: group '{group_name}' not found, skipping")
|
||||
continue
|
||||
if not conn.add_user_to_group(user_dn, group_id):
|
||||
print(f" WARNING: could not add to group '{group_name}': {conn.result}")
|
||||
else:
|
||||
print(f" Added to group '{group_name}'.")
|
||||
|
||||
if invite_mode:
|
||||
trigger_password_reset(username)
|
||||
print(f"User '{username}' created and invite email sent to '{effective_email}'.")
|
||||
else:
|
||||
print(f"User '{username}' created.")
|
||||
|
||||
|
||||
@app.command("remove", help="Remove a user")
|
||||
def cmd_remove(
|
||||
username: str = typer.Option(..., "--username", "-u", help="Username"),
|
||||
) -> None:
|
||||
validate_username(username)
|
||||
with lldap_connection() as conn:
|
||||
user_dn = f"uid={username},ou=people,{LLDAP_BASE_DN}"
|
||||
conn.search(user_dn, "(objectClass=*)", SUBTREE, attributes=["uid"])
|
||||
if not conn.entries:
|
||||
fail(f"user '{username}' not found")
|
||||
if not conn.delete(user_dn):
|
||||
fail(f"failed to delete user: {conn.result}")
|
||||
print(f"User '{username}' removed from LLDAP.")
|
||||
|
||||
|
||||
@app.command("reset-password", help="Trigger a password reset email for an existing user")
|
||||
def cmd_reset_password(
|
||||
username: str = typer.Option(..., "--username", "-u", help="Username"),
|
||||
confirm: bool = typer.Option(
|
||||
True,
|
||||
"--confirm/--no-confirm",
|
||||
help="Prompt for confirmation before sending the reset email",
|
||||
),
|
||||
) -> None:
|
||||
validate_username(username)
|
||||
with lldap_connection() as conn:
|
||||
if not user_exists(conn=conn, username=username):
|
||||
fail(f"user '{username}' not found")
|
||||
|
||||
if confirm:
|
||||
typer.confirm(
|
||||
f"Send password reset email to '{username}'?",
|
||||
abort=True,
|
||||
)
|
||||
|
||||
trigger_password_reset(username)
|
||||
print(f"Password reset email triggered for '{username}'.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
os.environ.setdefault("PYTHONUNBUFFERED", "1")
|
||||
sys.stdout.reconfigure(line_buffering=True)
|
||||
app()
|
||||
|
|
@ -114,11 +114,16 @@ resource "adguard_rewrite" "vault" {
|
|||
answer = "192.168.1.12"
|
||||
}
|
||||
|
||||
resource "adguard_rewrite" "pocketid" {
|
||||
resource "adguard_rewrite" "authelia-roboces" {
|
||||
domain = "auth.roboces.dev"
|
||||
answer = "192.168.1.12"
|
||||
}
|
||||
|
||||
resource "adguard_rewrite" "authelia" {
|
||||
domain = "auth.fuku"
|
||||
answer = "192.168.1.12"
|
||||
}
|
||||
|
||||
resource "adguard_rewrite" "dns" {
|
||||
domain = "dns.fuku"
|
||||
answer = "192.168.1.12"
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue