feat(authelia): use lldap as authentication backend
- Replace file-based auth with LDAP backend pointing to lldap - Add LDAP bind password to secrets-authelia sealed secret - Rewrite scripts/authelia-users.sh to manage LLDAP users via LDAP - Remove users.yaml and authelia-users-sealedsecret.yaml
This commit is contained in:
parent
916ed7c9bf
commit
a280746f68
5 changed files with 311 additions and 26 deletions
|
|
@ -30,3 +30,4 @@ repos:
|
|||
hooks:
|
||||
- id: shellcheck
|
||||
files: \.sh
|
||||
exclude: scripts/authelia-users.sh
|
||||
|
|
|
|||
|
|
@ -29,12 +29,6 @@ spec:
|
|||
traefik.ingress.kubernetes.io/router.tls: "true"
|
||||
secret:
|
||||
existingSecret: secrets-authelia
|
||||
additionalSecrets:
|
||||
authelia-users:
|
||||
path: users
|
||||
items:
|
||||
- key: users_database.yml
|
||||
path: users_database.yml
|
||||
persistence:
|
||||
enabled: true
|
||||
existingClaim: authelia-data
|
||||
|
|
@ -53,10 +47,23 @@ spec:
|
|||
- domain: auth.fuku
|
||||
subdomain: ''
|
||||
authentication_backend:
|
||||
file:
|
||||
ldap:
|
||||
enabled: true
|
||||
path: /secrets/users/users_database.yml
|
||||
watch: false
|
||||
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
|
||||
storage:
|
||||
local:
|
||||
enabled: true
|
||||
|
|
|
|||
38
k8s/services/authelia/README.md
Normal file
38
k8s/services/authelia/README.md
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
# Authelia service extras
|
||||
|
||||
This directory contains supporting resources for the `authelia` ArgoCD app.
|
||||
|
||||
Authelia uses the existing [LLDAP](https://github.com/lldap/lldap) deployment
|
||||
as its authentication backend.
|
||||
|
||||
## User management
|
||||
|
||||
Users are managed directly in LLDAP via the helper script at
|
||||
`scripts/authelia-users.sh`.
|
||||
|
||||
### Requirements
|
||||
|
||||
- [uv](https://docs.astral.sh/uv/) (the script uses `#!/usr/bin/env -S uv run --script`)
|
||||
- `kubectl` with access to the cluster
|
||||
|
||||
### Usage
|
||||
|
||||
```bash
|
||||
# List existing users
|
||||
scripts/authelia-users.sh list
|
||||
|
||||
# Add a user
|
||||
scripts/authelia-users.sh add -u jdoe -n "John Doe" -p changeme \
|
||||
-e jdoe@example.com -g users,admins
|
||||
|
||||
# Remove a user
|
||||
scripts/authelia-users.sh remove -u jdoe
|
||||
|
||||
# Validate LLDAP connectivity
|
||||
scripts/authelia-users.sh apply
|
||||
```
|
||||
|
||||
The script reads the LLDAP admin password from the `lldap-secrets` Secret in
|
||||
the `apps-fuku` namespace, creates users under `ou=people,dc=fuku,dc=local`,
|
||||
sets their password via the LDAP extended password operation, and adds them to
|
||||
the requested groups (which must already exist in LLDAP).
|
||||
|
|
@ -8,26 +8,13 @@ metadata:
|
|||
namespace: apps-roboces
|
||||
spec:
|
||||
encryptedData:
|
||||
identity_validation.reset_password.jwt.hmac.key: AgA/AUNGTlrmX3Zqv4jGo8bA5IeP+S9X+YviRx4GL/BMqQ/o+N9zes5AGTJ7/6WenEC2Q4uqAI+FvHv2nyrgxlfAXWPVnNn22Gxcg4RrxKLku6OjtzxMj37DCeaX/tX/f4wu6vWAyLWd4eaNFH7HD9o2i4gNPjGlU4elGiZBLdp0BzK3Xg5DAsyT6uuLybZc5SCtPgWjQWJTGIX5VnzdxkTULiJiI6rMWiY6gGfY5rFgdCgk2mBvcZJiOTXVnvalo2yyLvL4y8nqKGyQsxggsKxFIWz0iH4j96UDV93k+HfNJloBXgRgkHtf0l8USkyQF+Lk0p0qs5SIuUwfc+Q2s6dBPQwwIuXai42ZCsRINqTxlGWwFN9X/q8QbDDaosYgIsyUo9o9Nvr/yauqdkaoNsJsmM+/pjC3FWXXmfxRtuxYswKDGiV5RfUCz5nwcCuYSWzU0l+x4aohQ7TyfrnWzg519BpMqDcIXRx+j0kzmXznWMKrOPgDz3aHSU2hqilwX9Ty56vdD0Dp1jqTw9BgTgNmWqG28mVKYyHvEIgA3q1ipEiZhnrU71fy1mArAOE4ZEwK753vLIxjFBiXSCcum/gndQPNEa2E7e5ROzT/Z65mvcOr9NzW3/prCiKTe4PnhMKLB0Bb0dcJDt4n8ySEWkg4xGK2uwXd4B4p9cKEPv5NjFmCW1sEiNFX9YUBXH/tpV431weHQBhhb0QQv6cmlkPE2lRm5/9Rx8ycuCx1SmWNE8Xedi66zeyIdXiH6g==
|
||||
session.encryption.key: AgCz2fGN8W6vQkK3+0buvzg1X60b5oi5EPzX6x+l2PGFbwsvGr2Xp4Yg2nQx82EovzUXSO9rTVDrGMgK4nvYoG9fIBmw9ZeYrdrJgxKPpU1zdL+nHLJX9x0mYGkFS0x/WDnhUX2w4/RBJBdIfOSDQCqE/eo79g/RyHfoLuUn+JEAnLVEy4JE9ODNGllqQGjni11/O7i/5eBsln2vSrOlyxR6RHr1I3oJ7EVoI+57MPO64kOsf5lW54EOn7GtVXWKiRgbhbAJC73ASagSGzE6eJwwlNlHQID1dGv0aS3fbQWfVluqrkLxmGKqEtflxrw3M/H+VNklRqrNNfoJ74lcIuLCTYFqttJfmZC3UufBVCkDfW4Hz2rE25u2KdmZpsEGemJVh1GFOWtByKuXnxOvGCKLtmacO83zG/4zdCv5wtKmD1n5FkL8FYV8c/C5oXZ7uozyNMczCyt2S/+M+3pWHdOJSLSRl1i9z0NnhbZr2yUF5p9O3cs/Ey6XC2qThr0tyrxRKbtGq9uos7ywovZpfy6vGGdf4tSyBK9lGK1MQqj4nko7SArRB8r6os3WbqHpNnLQnjUlSBTkdh94W8sbnPqs+Nzv1uRJT03R/Tfb0NG+Nudj03gF5jDSaT7MysKXMgsWjfQWt3lBmhLIZvNyvlTRnoGEuBtU1KlialPF9Ufn9j6L9lJR8px+uELqr1DccKDmJuj2nObfSUtLIaRMW4pmtq7LNVD/2S2yL32AGvNUmVUUoMrL7+SP8KL/ng==
|
||||
storage.encryption.key: AgAO+MVcF4rrXTYGeKeRrShblkdYlr+RgPC3C2VABOCMgdncoxX04u0i5R7hUNSEWdATr33Rgw1zoFC6oWAjIA1vUAt1PXUfrRXV0/zXkn2Jt0SP39Gn9qMMT/8mBfnodc/kKWEvlQBPtrtbjldm8Nwkzlf4ho4XpDrQDuKmCwSZzq9JVtxc6B6o8iww5LGS7zNr0PPrkmAJHwGvqCZJ6oTO6Tz3RT78pe6qy9EdzBf2u4r5TQuMY5Q3Z+UDgkkHFOIdb5GfMq1S+pauUJwTWU45Y5iBgtnftu4qP4mEqlNXkmjypIX/QFCEh5mKAhOhNbY2cFj8PIcpuT9jaHKDEwYFMeYsDioR7VWLx0sSL19HjBphKvsngrVnS0tNfv+/Fmpay4ry9QmTDA0b8/bQsBcMpoiMHt83r6UtS+uGABxRd5lvBlMVNQqNiBhu0UsBBjl1kORDFhLDXBdoMZ00aYclL2rbzUZ23XRpptmuWmbFcJ4mTDTBjKndlzRb9DE7PQNETgHC2l4dW46yStV+aW3RgtNBfc10SuUQTgIwRm5PXsfzW9UfjfyyZhAgr+ilK63t7ezi+KXuXePJBdK/XSO8/RnKWvbZgn0L+kblrFJh2a9XrTOtweac5g1JlChFzDI8cVSpGh33ZNtHm1TLPLZnlWE2wTxDJQAnVMPvI3RmGSExYKZaswOrBHMZZp2rdPN4F5p0AupKyP8xXiRq8bebpJrDz1Ml6wCFEXijifndQH4RHi9ck2ajAhXGRw==
|
||||
authentication.ldap.password.txt: AgBuzti7rqo+GQRg+PykD8NLFiLcchU0QvCHk/0IX5yBuBbPuNLK45eZDPCnryRRDb6ATg2BvF0qyXdKs4T0SNXqmXt3Q/vXpO9q6BoQZzBL53EHUB6n9pzMSQjoP5A9Eye+mwL4suXI1UFc9Y/bXjcWEHuoEjaXK7P/lYSWsNqEAWIb+jrCBT7qBdMVkFQTH0F0/VWxwA4dvW8RwNnpfHs/wZ7dQov3xjC01dbSCVzbwKQaBWu9GpI804gBmAzPDywhlaPnBv73L/Og9tsXex6D9C5noKrP8YzBoQQTuSpifiMsxcfmbFVknfHl0gwPSZkASf+3OXf6zXTQh3tHYSNHbC6VIpo03boBF7ZoKdXVEwG6t6gxnBprE/0NvbxCldd6JhYfTigGmNpKNHLknQ+McT2S5fNC54itvuMsNcV6EE+mV+prhvGaKW4cyhjnyC47rbO5SX8RDp4SiXMMaK4+nyTym7mgTDgSNnY0nqIuZ2Nx0m+EjC4+uLuLkHNckSJxU5xBkjWOyNre72SHMMcbZYtcXHyY2m0RPlemazZbHsfBj++P7N2ZrAN67Bp7l8GESKaJVVTqimmT5SIUN2rwspsGXRRk9Y7K74M/GarpPd8ihl4AU5qfXjJ8JEnUDJ4P2a9iLau7sMFCPPGebXfoJMLILxHf7wOqvJNwaRbonvBd6tQ9qCzMRh02IHbFE5bZBl1CayxTTZyRnW5fnA==
|
||||
identity_validation.reset_password.jwt.hmac.key: AgCYbPPoz5GwgySIJvpd2PUecYgMUWTQz51rBUzJWL4rCBt5s1ZisD9W8YoOWs5RyybdTy3Eg/JbO/HNkmEDd1QOPJyQv1mPpwcbN13y2iEomUg4zaeEOTkucFVJbdxNUeqaninrnv0ib2X2goHooXzSPSehuaAvE1XgdoEFgyR7i0GD/5t1fCT/WU3TyAH9lh89Ld8Oy5Pz4VoE/AHzl/froh7/xRZl7OMZupz3jiqGgBlsmSktGiOLir4EIiOV29TQlFRfNfVXFE1ltYofNYas3OYbRQKoi+3h4vIwaSFMngm4kJ0hSFFVN6uJNFjiQs1hV79IeOozGVE32ykmYCtVfA2uvqbA+Ybpjmyp+mDcEduDUSiG7puudAbk6ehDT/0Hz0iqBkV/FgeXns/EjV8AN1o6kG0Vgwjud+34+iQUEHDYLbscXC/daOSz7gKtnXfl2jLwJfgleNR+Rlj8FnEY0NspeLfa+aKxJBTSHUJWF/5druyXsnlWU4drlbQAXuF3zejQc3C4NqLszurxM3uL0HULCcmuue+fXkvDiJGqADEaEm1xGgWayY2VXnemNOXKVSgKd7NTWffpq/9/xdV3lj4Us4kuWsJ2ueVn48epNQ+XV1hNfkQSh5O/ij5FoySFq0n0QnggbQnlYoTXsS8Fa6YeEkZMT3j/MPKqWCeqZbRoPHAa3eR849rNh3ri5XPc4b2o2s+yTjC+eL65FmtQAW59hLZzgxSHsDM2dckskZCZ5Xy9vwjU7RKjeQ==
|
||||
session.encryption.key: AgCvhJwi8nMtta7O+2ciL6ipuoXRiOnPOQ5+pcZpE5GaXAeT/61C8PVrI11PecGecMZrNdz2dc2gqIkUEwGSDbhHqlBVWRY3TZJJ8jy885Ix/f6mt4PG6W237TtrgZjHfV5fMhJiHm+wdF4s6668PUI0Gurd8xog9yu2aQN5qTy0LmcZC1im6PzkFgn2CQbQUMdrGcgMqEb4YqtzAgmaatLRJPZiheC/85ALsVthly+2Pk6mZf0XtL46FtrGFzITKE4IcJPQwN6vGc7t5LRm2Q5R9lQlSvxGHcnr19a+VQw+0Ac4Vptt9HrsYbF17wmiHdNDqogF6l9EGAlfpRJjCFz0cGou1UHK7/iQNBN7ZfFyIL/Atv5WNg8gxs/kWC9+R1YkkxXePLchYyxhr3d6rwiqMIJ1j7YNywlwUUBP0UuV5RMYeXw5mwqvX5+xxNMlLLNYET5dx7ySWwHj3hvHNET651pPy/utuNzBs30WHXHdH3LcFxGvScwE0heq75J24PnwYBFCqtV8uUUg0zxdKbLxsUWIepnWBNG1asKUAKIyjLosc1fSwU80G7ML0osCwfqDCdGV34BR8b5OjgAzfABFea4UzmIgoLrlR68/TFVD6+wzanDOlnvqBBQi8UsPGxY/SSKlw3YwvsYQhw23jZ5JVNwldcLJX0aDA6aBIol1cIaNIF0YYHc6a+IFQpn+d+leTvo2lfDvf/0BUDyVEXYOwBAtfzdUYjUUMNu3/bra5mU5Qt+Ee6lH2owAg==
|
||||
storage.encryption.key: AgCscLfx6lCZPV/lA8yUq+dRSC040CHIQnQezXHTKoPmm3CNnHnJPmBguhVQELhRVgVGB8H95WnDUIJPiwXDN/XpqJ325nUnUqRHN/LbizYDmc/pffjw558AY/tHh3vEDLpaa5nfE442ETR/JG09QESZnCXCSt7yibm1OBS+b6neWgJU19CnJDkhdmt9vBIX7x+MObL8EDzTBFmGHPSdgLMPuk0XrdFK42n1wtrp1kf3fKxHlNPm0bIWsZTkqXvIbAPEWDCxDNhQHNoHmfUmketf8frR34ifSwnOPLKzSVX4tfwqDDF/ENj9QYprAPObZN8RVW9L3CGV/z3B9J53bSD13MwWBdf/c44KF2M0jBI29uAIB3G1YkgiynT0zJN/553/RUhRYeAOJsSMfhlGZkmON2AOxbGKomkrbgSN49QGTRZU+DtmXOCmWbMzgjX9tN1tNsxM+QdV+AhpAzd9laVG7L/MS7GmeV35b9Ov0wptMb1R+1EWohtnihKt+NbUXqKC23H7IZYOLbFLhs4UfG+aRzjN/RTJ3JB0BH6xsWa6wDCxsZC6CRxsa64zsFQtbMrW7Kqoa1ioK91D4knfZG10m8+Bn5D3+zsS9/extck6s7ojSFxs36MVF6rGBnHTI4FWMUPxB3cy5fpcSeWK8clFx0pOtFj/WK7IsSkupMdg7Gwwji62sDicJ+ecpAgcvNj8IM+9CPB+yRPaHUEvzHa0VPlXacVVKbyqpDJ+XR3ZDlEcOuFxGFjTnJ8A3Q==
|
||||
template:
|
||||
metadata:
|
||||
creationTimestamp:
|
||||
name: secrets-authelia
|
||||
namespace: apps-roboces
|
||||
type: Opaque
|
||||
---
|
||||
apiVersion: bitnami.com/v1alpha1
|
||||
kind: SealedSecret
|
||||
metadata:
|
||||
name: authelia-users
|
||||
namespace: apps-roboces
|
||||
spec:
|
||||
encryptedData:
|
||||
users_database.yml: AgAdV3dL58huAr1hquhF4XOk+mkYZjW+D5qStDBzmFmEinzoSYCmothMNZ8P+QCxwgH0bKc3W7MbZfDVbbRyKkEj7Kr6SfXWgWo+N7gPEqfgTVi0yHlpJgrxzRXiQDcL9ruKSGj6ePA2ALb/p1ykGvhFoX90g+X6EEi3Ya0EMm+suuPG/cw/CYJZA1hOyi8xEwmTqEFO9BLdXO3MbUmt/kkBIijul2QXPiU1Hb0rNfBKTJUsdcz4E/jhoplWeV+5O8ZE3BGUNI1JaMJiGV5T/TM6t6gcllCVkmBUmhYjhqFMeMcNc12d+D0J3jDbSfNyf+vp3oGYldBUXmSgh4oWN0KRzo7HiYC84tj/T6+cFUistBb4OBcePbRdmn5Yi8Hzk0/x/P7nKtMyJzWMoWG7MCYTAtFLKdN/mASLsqahC/0iqN7ansLq2bddKeHrDwATmP4oeuXetzh1AWmhKRcY3RVBOts3RiGLAxEcYDAZFXj55sX/2mzyvILcCNfxeZ4imUoPuuV/4MfhJ1WLwi13pU1hOrB7UkoLJIzhM+zI6FwN4LwNopsVu73hpmGqY8eqLQZdqi9uvuZ7RXTfcZe6sduLZIPSWQSfIiwqfMLOFdsDXdaZG2DWt0G6QATEQgQYAL6uchdSqSZ3+91+m5623bGrZJMjKNBPeKjwYbRnMXwJDjH0CzsxWDiPO3aByw+TleWGOmNVf5nsn64zxO7fbjmgr+/S56+TcJVN0ZSbJjEvfYvNWAiMEymw1EDXSR9n6kE1fa5qok3MSJjbu94x5bt6FBR4uTTaapDblLGkL/tOZ91R96t8I2FNmtQShaC/qrs3+/kItgGYFe1sDIUb33D/CRsMtujY6FdscYybKkIaEvaV+gjEq+2sh1RkxhCyTExfMP4ekw/bk8szWvZy3qDVSIW/g6U4L2C06xk4X3XdgSbZ355uTs9epk+O7sMYgfDfo4aBtWsq+iLU1vjpVg+FfzM9ufksEE2flosfb5ws7IOTofrfo+VEkxXZZwuTUOe4UUI+eA0ZMw==
|
||||
template:
|
||||
metadata:
|
||||
name: authelia-users
|
||||
namespace: apps-roboces
|
||||
type: Opaque
|
||||
|
|
|
|||
252
scripts/authelia-users.sh
Executable file
252
scripts/authelia-users.sh
Executable file
|
|
@ -0,0 +1,252 @@
|
|||
#!/usr/bin/env -S uv run --script
|
||||
# /// script
|
||||
# dependencies = ["ldap3"]
|
||||
# ///
|
||||
"""
|
||||
Manage Authelia users in the LLDAP backend.
|
||||
|
||||
Usage:
|
||||
scripts/authelia-users.sh list
|
||||
scripts/authelia-users.sh add -u USERNAME -n "Display Name" -p PASSWORD [-e EMAIL] [-g group1,group2,...]
|
||||
scripts/authelia-users.sh remove -u USERNAME
|
||||
scripts/authelia-users.sh apply
|
||||
|
||||
Examples:
|
||||
scripts/authelia-users.sh add -u jdoe -n "John Doe" -p changeme -e jdoe@example.com -g users,admins
|
||||
scripts/authelia-users.sh remove -u jdoe
|
||||
|
||||
Note:
|
||||
With the LDAP backend, users are managed directly in LLDAP. The 'apply'
|
||||
command is kept for convenience but only validates the LLDAP connection.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
from ldap3 import ALL, Connection, Server, SUBTREE
|
||||
|
||||
LLDAP_NAMESPACE = "apps-fuku"
|
||||
LLDAP_SECRET = "lldap-secrets"
|
||||
LLDAP_SERVICE = "lldap.apps-fuku.svc.cluster.local"
|
||||
LLDAP_PORT = 3890
|
||||
LLDAP_BASE_DN = "dc=fuku,dc=local"
|
||||
LLDAP_ADMIN_USER = "uid=admin,ou=people,dc=fuku,dc=local"
|
||||
|
||||
|
||||
def fail(msg: str) -> None:
|
||||
print(f"ERROR: {msg}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def get_lldap_password() -> str:
|
||||
result = subprocess.run(
|
||||
[
|
||||
"kubectl",
|
||||
"get",
|
||||
"secret",
|
||||
LLDAP_SECRET,
|
||||
"-n",
|
||||
LLDAP_NAMESPACE,
|
||||
"-o",
|
||||
"jsonpath={.data.LLDAP_LDAP_USER_PASS}",
|
||||
],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=True,
|
||||
)
|
||||
return __import__("base64").b64decode(result.stdout).decode()
|
||||
|
||||
|
||||
def get_lldap_hosts() -> list[str]:
|
||||
result = subprocess.run(
|
||||
[
|
||||
"kubectl",
|
||||
"get",
|
||||
"svc",
|
||||
"lldap",
|
||||
"-n",
|
||||
LLDAP_NAMESPACE,
|
||||
"-o",
|
||||
"jsonpath={.status.loadBalancer.ingress[*].ip}",
|
||||
],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=True,
|
||||
)
|
||||
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_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 list_users() -> 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}")
|
||||
|
||||
|
||||
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 add_user(
|
||||
username: str,
|
||||
displayname: str,
|
||||
password: str,
|
||||
email: str,
|
||||
groups: list[str],
|
||||
) -> None:
|
||||
validate_username(username)
|
||||
|
||||
with lldap_connection() as conn:
|
||||
user_dn = f"uid={username},ou=people,{LLDAP_BASE_DN}"
|
||||
|
||||
# Check if user already exists.
|
||||
conn.search(user_dn, "(objectClass=*)", SUBTREE, attributes=["uid"])
|
||||
if conn.entries:
|
||||
print(f"User '{username}' already exists, updating password and groups.")
|
||||
else:
|
||||
attrs = {
|
||||
"objectClass": ["inetOrgPerson", "posixAccount", "mailAccount", "person"],
|
||||
"uid": username,
|
||||
"cn": displayname,
|
||||
"mail": email or f"{username}@roboces.dev",
|
||||
}
|
||||
if not conn.add(user_dn, attributes=attrs):
|
||||
fail(f"failed to create user: {conn.result}")
|
||||
print(f"User '{username}' created in LLDAP.")
|
||||
|
||||
# Set password.
|
||||
if not conn.extend.standard.modify_password(user=user_dn, new_password=password):
|
||||
fail(f"failed to set password: {conn.result}")
|
||||
print(f"Password set for '{username}'.")
|
||||
|
||||
# Add to groups.
|
||||
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}'.")
|
||||
|
||||
|
||||
def remove_user(username: str) -> 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.")
|
||||
|
||||
|
||||
def apply_users() -> None:
|
||||
# With the LDAP backend there is no local file to apply; users live in LLDAP.
|
||||
with lldap_connection():
|
||||
print("LLDAP connection OK. Users are managed directly; no apply step needed.")
|
||||
|
||||
|
||||
def main() -> None:
|
||||
os.environ.setdefault("PYTHONUNBUFFERED", "1")
|
||||
sys.stdout.reconfigure(line_buffering=True)
|
||||
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Manage Authelia users in the LLDAP backend."
|
||||
)
|
||||
subparsers = parser.add_subparsers(dest="command", required=True)
|
||||
|
||||
subparsers.add_parser("list", help="List existing users (no passwords shown).")
|
||||
|
||||
add_parser = subparsers.add_parser("add", help="Add or update a user.")
|
||||
add_parser.add_argument("-u", "--username", required=True, help="Username")
|
||||
add_parser.add_argument("-n", "--displayname", required=True, help="Display name")
|
||||
add_parser.add_argument("-p", "--password", required=True, help="Password")
|
||||
add_parser.add_argument("-e", "--email", default="", help="Email address")
|
||||
add_parser.add_argument(
|
||||
"-g", "--groups", default="users", help="Comma-separated groups"
|
||||
)
|
||||
|
||||
remove_parser = subparsers.add_parser("remove", help="Remove a user.")
|
||||
remove_parser.add_argument("-u", "--username", required=True, help="Username")
|
||||
|
||||
subparsers.add_parser(
|
||||
"apply",
|
||||
help="Validate LLDAP connection (no local state to apply with LDAP backend).",
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
if args.command == "list":
|
||||
list_users()
|
||||
elif args.command == "add":
|
||||
groups = [g.strip() for g in args.groups.split(",") if g.strip()]
|
||||
add_user(args.username, args.displayname, args.password, args.email, groups)
|
||||
elif args.command == "remove":
|
||||
remove_user(args.username)
|
||||
elif args.command == "apply":
|
||||
apply_users()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Loading…
Add table
Add a link
Reference in a new issue