add authz group

This commit is contained in:
2026-10-13 15:05:04 +07:00
parent f534e8da54
commit 5519ae2de7
4 changed files with 483 additions and 38 deletions
+61 -8
View File
@@ -25,6 +25,8 @@ from pathlib import Path
import yaml import yaml
import authz_ldap as L
CAPS = ("select", "write", "ddl") CAPS = ("select", "write", "ddl")
DEPLOY_DIR = Path(__file__).resolve().parent.parent DEPLOY_DIR = Path(__file__).resolve().parent.parent
DEFAULT_MODEL = DEPLOY_DIR / "config" / "opa" / "authz.yml" DEFAULT_MODEL = DEPLOY_DIR / "config" / "opa" / "authz.yml"
@@ -425,21 +427,54 @@ def cmd_role(args, model_path: Path) -> int:
def cmd_user(args, model_path: Path) -> int: def cmd_user(args, model_path: Path) -> int:
model = load_model(model_path) model = load_model(model_path)
users = model.setdefault("users", {}) users_ = model.setdefault("users", {})
if args.action == "assign": if args.action == "assign":
if args.role not in model.get("roles", {}): if args.role not in model.get("roles", {}):
raise ModelError(f"role {args.role!r} tidak ada") raise ModelError(f"role {args.role!r} tidak ada")
users[args.user] = args.role users_[args.user] = args.role
_say(f"assign {args.user} -> {args.role}") _say(f"assign {args.user} -> {args.role}")
else: else:
if args.user not in users: if args.user not in users_:
raise ModelError(f"service account {args.user!r} tidak terdaftar") raise ModelError(f"service account {args.user!r} tidak terdaftar")
del users[args.user] del users_[args.user]
_say(f"unassign {args.user}") _say(f"unassign {args.user}")
save_model(model_path, model) save_model(model_path, model)
return 0 return 0
def cmd_user_ldap(args) -> int:
conf = L.config()
if args.action == "list":
return L.user_list(conf, args.service)
if not args.user:
raise ModelError(f"user {args.action}: butuh <uid>")
if args.action == "add":
return L.user_add(conf, args.user, args.cn, args.mail, args.password, args.group)
return L.user_remove(conf, args.user)
def cmd_group(args, model_path: Path) -> int:
conf = L.config()
a = args.action
if a == "list":
return L.group_list(conf, args.members)
if a == "check":
return L.group_check(conf, model_path)
if a == "sync":
return L.group_sync(conf, model_path, args.apply)
if not args.a:
raise ModelError(f"group {a}: butuh <cn>")
if a == "add":
return L.group_add(conf, args.a, args.member)
if a == "remove":
return L.group_remove(conf, args.a)
if not args.b:
raise ModelError(f"group {a}: butuh dua argumen")
if a == "rename":
return L.group_rename(conf, model_path, args.a, args.b)
return L.group_member(conf, args.a, args.b, a == "add-member")
CATALOG_SCAN_PATTERNS = ("deploy/scripts", "deploy/lib", "deploy/config", "docs") CATALOG_SCAN_PATTERNS = ("deploy/scripts", "deploy/lib", "deploy/config", "docs")
@@ -562,9 +597,23 @@ def build_parser() -> argparse.ArgumentParser:
pr.add_argument("--force", action="store_true") pr.add_argument("--force", action="store_true")
pu = sub.add_parser("user") pu = sub.add_parser("user")
pu.add_argument("action", choices=["assign", "unassign"]) pu.add_argument("action", choices=["assign", "unassign", "list", "add", "remove"])
pu.add_argument("user") pu.add_argument("user", nargs="?")
pu.add_argument("role", nargs="?") pu.add_argument("role", nargs="?")
pu.add_argument("--service", action="store_true", help="user list: tampilkan service account (model)")
pu.add_argument("--cn")
pu.add_argument("--mail")
pu.add_argument("--password")
pu.add_argument("--in", dest="group")
pgr = sub.add_parser("group")
pgr.add_argument("action", choices=["list", "add", "remove", "rename",
"add-member", "remove-member", "check", "sync"])
pgr.add_argument("a", nargs="?", help="cn (atau cn lama untuk rename)")
pgr.add_argument("b", nargs="?", help="uid (atau cn baru)")
pgr.add_argument("--members", action="store_true", help="list: tampilkan anggota")
pgr.add_argument("--member", action="append", default=[], help="add: uid anggota (boleh berulang)")
pgr.add_argument("--apply", action="store_true", help="sync: tulis perubahan")
pc = sub.add_parser("catalog") pc = sub.add_parser("catalog")
pc.add_argument("action", choices=["list", "add", "rename"]) pc.add_argument("action", choices=["list", "add", "rename"])
@@ -591,7 +640,11 @@ def dispatch(args) -> int:
if args.cmd == "role": if args.cmd == "role":
return cmd_role(args, args.model) return cmd_role(args, args.model)
if args.cmd == "user": if args.cmd == "user":
return cmd_user(args, args.model) if args.action in ("assign", "unassign"):
return cmd_user(args, args.model)
return cmd_user_ldap(args)
if args.cmd == "group":
return cmd_group(args, args.model)
if args.cmd == "catalog": if args.cmd == "catalog":
return cmd_catalog(args, args.model) return cmd_catalog(args, args.model)
raise ModelError(f"perintah tak dikenal: {args.cmd}") raise ModelError(f"perintah tak dikenal: {args.cmd}")
@@ -601,7 +654,7 @@ def main(argv: list | None = None) -> int:
args = build_parser().parse_args(argv) args = build_parser().parse_args(argv)
try: try:
return dispatch(args) return dispatch(args)
except ModelError as exc: except (ModelError, L.LdapError) as exc:
print(f"ERROR: {exc}", file=sys.stderr) print(f"ERROR: {exc}", file=sys.stderr)
return 2 return 2
+354
View File
@@ -0,0 +1,354 @@
#!/usr/bin/env python3
# =============================================================================
# SiCABE — lib/authz_ldap.py
# -----------------------------------------------------------------------------
# Pengelolaan GRUP & USER OpenLDAP (sumber grup Trino & StarRocks) untuk CLI
# sicabe-authz. Melengkapi model OPA: nama grup LDAP (cn) WAJIB sama dengan kunci
# `roles` di config/opa/authz.yml DAN `permitted_groups` di bootstrap.sh.
#
# Strategi tulis: SELALU coba tulis (via ldapadd/ldapmodify/ldapdelete). Bila
# gagal (mis. LDAP existing read-only), LDIF yang setara dicetak agar bisa
# diterapkan admin LDAP secara manual.
#
# Semua operasi memakai ldap-utils; bila container openldap LOKAL berjalan,
# perintah dijalankan lewat `docker exec` (tak perlu ldap-utils di host).
# =============================================================================
from __future__ import annotations
import os
import re
import subprocess
import sys
DRY = os.environ.get("SICABE_AUTHZ_DRY_RUN") == "1"
class LdapError(Exception):
"""Operasi LDAP gagal / konfigurasi tak lengkap."""
def _say(msg: str) -> None:
print(f"[DRY-RUN] {msg}" if DRY else f"{msg}")
# ── Konfigurasi & eksekusi ──────────────────────────────────────────────────
def config() -> dict:
return {
"container": os.environ.get("LDAP_CONTAINER_NAME", "sicabe-openldap"),
"admin_dn": os.environ.get("LDAP_ADMIN_DN", "cn=admin,dc=sicabe,dc=local"),
"password": os.environ.get("LDAP_ADMIN_PASSWORD", ""),
"host": os.environ.get("LDAP_HOST", "localhost"),
"port": os.environ.get("LDAP_PORT") or os.environ.get("OPENLDAP_PORT", "12389"),
"users_dn": os.environ.get("LDAP_USERS_DN", "ou=users,dc=sicabe,dc=local"),
"groups_dn": os.environ.get("LDAP_GROUPS_DN", "ou=groups,dc=sicabe,dc=local"),
}
def is_local(conf: dict) -> bool:
r = subprocess.run(["docker", "ps", "--format", "{{.Names}}"], capture_output=True, text=True)
return conf["container"] in r.stdout.split()
def endpoint(conf: dict) -> str:
return "ldap://localhost" if is_local(conf) else f"ldap://{conf['host']}:{conf['port']}"
def _run(conf: dict, args: list, stdin: str | None = None):
cmd = (["docker", "exec", "-i", conf["container"]] if is_local(conf) else []) + args
return subprocess.run(cmd, input=stdin, capture_output=True, text=True)
def _search(conf: dict, base: str, filt: str, attrs: list | None = None) -> list:
args = ["ldapsearch", "-LLL", "-x", "-H", endpoint(conf), "-D", conf["admin_dn"],
"-w", conf["password"], "-b", base, filt]
if attrs:
args += attrs
r = _run(conf, args)
if r.returncode != 0 and "No such object" not in r.stderr:
raise LdapError(f"ldapsearch gagal: {r.stderr.strip() or 'periksa LDAP_URL/kredensial'}")
return parse_ldif(r.stdout)
def _apply(conf: dict, tool: str, ldif: str, action: str) -> None:
"""Jalankan ldapadd/ldapmodify/ldapdelete; bila gagal, cetak LDIF setara."""
if DRY:
_say(f"{tool} ({action})"); print(_indent(ldif)); return
r = _run(conf, [tool, "-x", "-H", endpoint(conf), "-D", conf["admin_dn"],
"-w", conf["password"]], stdin=ldif)
if r.returncode != 0:
print(f" GAGAL: {r.stderr.strip()}", file=sys.stderr)
print(" LDIF yang setara (untuk diterapkan admin LDAP):", file=sys.stderr)
print(_indent(ldif), file=sys.stderr)
raise LdapError(f"{tool} gagal untuk {action}")
_say(f"{tool} ok: {action}")
def _delete(conf: dict, dn: str, action: str) -> None:
"""ldapdelete TIDAK membaca LDIF dari stdin — DN diberikan sebagai argumen."""
if DRY:
_say(f"ldapdelete ({action})"); print(_indent(f"dn: {dn}")); return
r = _run(conf, ["ldapdelete", "-x", "-H", endpoint(conf), "-D", conf["admin_dn"],
"-w", conf["password"], dn])
if r.returncode != 0:
print(f" GAGAL: {r.stderr.strip()}", file=sys.stderr)
print(f" DN: {dn}", file=sys.stderr)
raise LdapError(f"ldapdelete gagal untuk {action}")
_say(f"ldapdelete ok: {action}")
def _indent(text: str) -> str:
return "\n".join(" " + ln for ln in text.strip().splitlines())
def parse_ldif(text: str) -> list:
entries, cur, prev = [], {}, None
for raw in text.splitlines():
if raw.startswith("#"):
continue
if not raw.strip():
if cur:
entries.append(cur)
cur, prev = {}, None
continue
if raw[:1] == " " and prev: # lanjutan (folded line)
cur[prev] = cur[prev] + raw[1:]
continue
if ":" not in raw:
continue
k, _, v = raw.partition(":")
k = k.strip()
v = v.strip()
if k in cur:
if not isinstance(cur[k], list):
cur[k] = [cur[k]]
cur[k].append(v)
else:
cur[k] = v
prev = k
if cur:
entries.append(cur)
return entries
# ── Query LDAP ──────────────────────────────────────────────────────────────
def groups(conf: dict) -> dict:
out = {}
for e in _search(conf, conf["groups_dn"], "(objectClass=groupOfNames)", ["cn", "member"]):
cn = e.get("cn")
if cn:
m = e.get("member", [])
out[cn] = m if isinstance(m, list) else [m]
return out
def users(conf: dict) -> list:
return sorted(e["uid"] for e in _search(conf, conf["users_dn"], "(objectClass=inetOrgPerson)", ["uid"])
if e.get("uid"))
def _user_dn(conf: dict, uid: str) -> str:
return f"uid={uid},{conf['users_dn']}"
def _group_dn(conf: dict, cn: str) -> str:
return f"cn={cn},{conf['groups_dn']}"
def _next_number(conf: dict) -> int:
nums = [int(e["uidNumber"]) for e in _search(conf, conf["users_dn"], "(uidNumber=*)", ["uidNumber"])
if str(e.get("uidNumber", "")).isdigit()]
return (max(nums) + 1) if nums else 1000
# ── Perintah: group ─────────────────────────────────────────────────────────
def group_list(conf: dict, with_members: bool) -> int:
for cn, members in sorted(groups(conf).items()):
print(f"{cn} ({len(members)} member)" if not with_members else cn)
if with_members:
for m in members:
print(f" {m}")
return 0
def group_add(conf: dict, cn: str, member_uids: list) -> int:
if cn in groups(conf):
raise LdapError(f"grup {cn!r} sudah ada")
if not member_uids:
raise LdapError("groupOfNames butuh minimal 1 member — beri --member <uid>")
lines = [f"dn: {_group_dn(conf, cn)}", "objectClass: groupOfNames", f"cn: {cn}"]
for uid in member_uids:
lines.append(f"member: {_user_dn(conf, uid)}")
_apply(conf, "ldapadd", "\n".join(lines) + "\n", f"buat grup {cn}")
return 0
def group_remove(conf: dict, cn: str) -> int:
if cn not in groups(conf):
raise LdapError(f"grup {cn!r} tidak ada")
_delete(conf, _group_dn(conf, cn), f"hapus grup {cn}")
return 0
def group_member(conf: dict, cn: str, uid: str, add: bool) -> int:
if cn not in groups(conf):
raise LdapError(f"grup {cn!r} tidak ada")
verb = "add" if add else "delete"
ldif = (f"dn: {_group_dn(conf, cn)}\nchangetype: modify\n{verb}: member\n"
f"member: {_user_dn(conf, uid)}\n")
_apply(conf, "ldapmodify", ldif, f"{'tambah' if add else 'hapus'} {uid} di {cn}")
return 0
def group_rename(conf: dict, model_path, old: str, new: str) -> int:
import authz as A
if old not in groups(conf):
raise LdapError(f"grup {old!r} tidak ada")
if new in groups(conf):
raise LdapError(f"grup {new!r} sudah ada")
model = A.load_model(model_path)
if old not in model.get("roles", {}):
raise LdapError(f"role {old!r} tidak ada di {model_path} — nama grup LDAP harus sama dengan kunci roles")
# 1) LDAP: rename entry (modrdn)
ldif = (f"dn: {_group_dn(conf, old)}\nchangetype: modrdn\nnewrdn: cn={new}\n"
f"deleteoldrdn: 1\n")
_apply(conf, "ldapmodify", ldif, f"rename grup LDAP {old} -> {new}")
# 2) model OPA: ganti kunci role
roles = model["roles"]
model["roles"] = { (new if k == old else k): v for k, v in roles.items() }
A.save_model(model_path, model)
_say(f"authz.yml: roles {old} -> {new}")
# 3) StarRocks permitted_groups di bootstrap.sh
_update_permitted_groups(old, new)
return 0
# ── Perintah: user ──────────────────────────────────────────────────────────
def user_list(conf: dict, service: bool) -> int:
if service:
import authz as A
for u, r in (A.load_model(A.DEFAULT_MODEL).get("users", {}) or {}).items():
print(f"{u} -> {r} (service account)")
return 0
for uid in users(conf):
print(uid)
return 0
def user_add(conf: dict, uid: str, cn: str | None, mail: str | None,
password: str | None, group: str | None) -> int:
if uid in users(conf):
raise LdapError(f"user {uid!r} sudah ada")
n = _next_number(conf)
lines = [f"dn: {_user_dn(conf, uid)}", "objectClass: inetOrgPerson", "objectClass: posixAccount",
f"uid: {uid}", f"cn: {cn or uid}", f"sn: {cn or uid}", f"mail: {mail or uid + '@sicabe.local'}",
f"uidNumber: {n}", f"gidNumber: {n}", f"homeDirectory: /home/{uid}"]
if password:
lines.append(f"userPassword: {password}")
_apply(conf, "ldapadd", "\n".join(lines) + "\n", f"buat user {uid}")
if group:
group_member(conf, group, uid, add=True)
print(f" catatan: pastikan user Keycloak dengan username '{uid}' ada (auth = Keycloak OIDC).")
return 0
def user_remove(conf: dict, uid: str) -> int:
if uid not in users(conf):
raise LdapError(f"user {uid!r} tidak ada")
dn = _user_dn(conf, uid)
for cn, members in groups(conf).items():
if dn in members:
group_member(conf, cn, uid, add=False)
_delete(conf, dn, f"hapus user {uid}")
return 0
# ── Konsistensi: grup LDAP == roles OPA == permitted_groups StarRocks ────────
BS_RE = re.compile(r"('permitted_groups'\s*=\s*')([^']*)(')")
def _bootstrap_path() -> str:
# Bisa ditimpa (untuk pengujian): SICABE_BOOTSTRAP=/path/ke/bootstrap.sh
return os.environ.get("SICABE_BOOTSTRAP") or os.path.join(
os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "bootstrap.sh")
def permitted_groups() -> list:
with open(_bootstrap_path(), encoding="utf-8") as fh:
for line in fh:
m = BS_RE.search(line)
if m:
return [g for g in m.group(2).split(",") if g]
return []
def _update_permitted_groups(old: str, new: str) -> None:
path = _bootstrap_path()
with open(path, encoding="utf-8") as fh:
text = fh.read()
if f"'{old}'" not in text and f",{old}," not in text and not re.search(rf"\b{re.escape(old)}\b", text):
return
new_text = BS_RE.sub(lambda m: m.group(1) + ",".join(new if g == old else g for g in m.group(2).split(",")) + m.group(3), text, count=1)
if new_text == text:
print(f" (permitted_groups tidak memuat {old})")
return
if DRY:
_say(f"bootstrap.sh: permitted_groups {old} -> {new}")
else:
with open(path, "w", encoding="utf-8") as fh:
fh.write(new_text)
print(f" bootstrap.sh: permitted_groups {old} -> {new}")
def group_check(conf: dict, model_path) -> int:
import authz as A
roles = set(A.load_model(model_path).get("roles", {}))
cns = set(groups(conf))
permitted = set(permitted_groups())
problems = 0
for c in sorted(cns - roles):
print(f" ❌ grup LDAP {c!r} tidak punya role di authz.yml"); problems += 1
for g in sorted(permitted - roles):
print(f" ❌ permitted_groups memuat {g!r} yang bukan role"); problems += 1
if cns != permitted:
only_cn, only_pg = sorted(cns - permitted), sorted(permitted - cns)
if only_cn:
print(f" ❌ grup LDAP tanpa permitted_groups: {only_cn}"); problems += 1
if only_pg:
print(f" ❌ permitted_groups tanpa grup LDAP: {only_pg}"); problems += 1
no_group = sorted(roles - cns)
if no_group:
print(f" ℹ role tanpa grup LDAP (mis. khusus service account): {no_group}")
if problems == 0:
print(f" ✅ konsisten: grup LDAP == permitted_groups ⊆ roles ({len(cns)} grup)")
else:
print(f" {problems} masalah · perbaiki dgn: sicabe-authz group sync / group add")
return 1 if problems else 0
def group_sync(conf: dict, model_path, apply: bool) -> int:
import authz as A
roles = set(A.load_model(model_path).get("roles", {}))
cns = sorted(groups(conf))
for c in cns:
if c not in roles:
print(f" ❌ grup LDAP {c!r} tanpa role di authz.yml — tambahkan: sicabe-authz role add {c}")
# permitted_groups StarRocks = daftar GRUP LDAP (bukan semua role). Setara -> biarkan.
if set(permitted_groups()) == set(cns):
print(f" permitted_groups sudah sinkron ({','.join(permitted_groups())})")
return 0
path = _bootstrap_path()
with open(path, encoding="utf-8") as fh:
text = fh.read()
new_text = BS_RE.sub(lambda m: m.group(1) + ",".join(cns) + m.group(3), text, count=1)
if apply and not DRY:
with open(path, "w", encoding="utf-8") as fh:
fh.write(new_text)
print(f" bootstrap.sh: permitted_groups -> {','.join(cns)}")
else:
_say(f"bootstrap.sh: permitted_groups -> {','.join(cns)}")
return 0
+47 -28
View File
@@ -2,53 +2,72 @@
# ============================================================================= # =============================================================================
# eBdesk SiCABE — sicabe-authz # eBdesk SiCABE — sicabe-authz
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
# CLI pengelolaan AUTHORIZATION (OPA/Trino) berbasis model config/opa/authz.yml. # CLI pengelolaan AUTHORIZATION: model OPA/Trino (config/opa/authz.yml) dan
# GRUP/USER OpenLDAP (sumber grup Trino & StarRocks). Logika: lib/authz.py +
# lib/authz_ldap.py (Python + PyYAML; ldap-utils).
# #
# Model authz.yml = SUMBER KEBENARAN. Perintah di sini mengedit model, lalu # Pemakaian: ./scripts/sicabe-authz <perintah> [opsi] [--dry-run] [--env-file <f>]
# merender data ACL OPA (config/opa/data.json) dan (opsional) memuat ulang OPA. # --dry-run (posisi mana pun) -> jangan tulis apa pun, cetak rencana.
# Logika model ada di lib/authz.py (Python + PyYAML).
#
# Pemakaian:
# ./scripts/sicabe-authz list roles
# ./scripts/sicabe-authz matrix [--schema bronze]
# ./scripts/sicabe-authz show role data-engineer
# ./scripts/sicabe-authz grant set data-analyst gold.orders_gold --caps select --columns id
# ./scripts/sicabe-authz role add data-scientist
# ./scripts/sicabe-authz user assign svc_dbt data-engineer
# ./scripts/sicabe-authz catalog rename iceberg lakehouse
# ./scripts/sicabe-authz render # authz.yml -> data.json
# ./scripts/sicabe-authz check # pastikan data.json sinkron
# ./scripts/sicabe-authz apply [--no-reload] # render + reload OPA
# ./scripts/sicabe-authz reload # recreate container OPA
#
# --dry-run (posisi mana pun) -> jangan tulis apa pun, hanya cetak rencana.
# ============================================================================= # =============================================================================
set -euo pipefail set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
# shellcheck source=../lib/common.sh # shellcheck source=../lib/common.sh
source "${SCRIPT_DIR}/lib/common.sh" 2>/dev/null || true source "${SCRIPT_DIR}/lib/common.sh" 2>/dev/null || true
# shellcheck source=../config/environment.defaults.sh
source "${SCRIPT_DIR}/config/environment.defaults.sh" 2>/dev/null || true
OPA_CONTAINER="${OPA_CONTAINER_NAME:-sicabe-opa}" OPA_CONTAINER="${OPA_CONTAINER_NAME:-sicabe-opa}"
PY="${SCRIPT_DIR}/lib/authz.py"
usage() { usage() {
sed -n '3,27p' "${BASH_SOURCE[0]}" | sed 's/^# \{0,1\}//' cat <<'EOF'
sicabe-authz — kelola authorization (OPA/Trino) + grup/user OpenLDAP
MODEL OPA (authz.yml -> data.json):
list catalogs|roles|users|tables daftar isi model
matrix [--schema <s>] matriks role x tabel (s=select w=write d=ddl)
show role|user|table <nama> detail satu objek
grant add|set|remove <role> <schema.table> [--caps a,b] [--columns x,y|*]
role add|remove <nama> [--superuser] [--force]
user assign|unassign <svc> <role> service account -> role (model)
catalog list|add|rename <baru> kelola catalog (rename ikut berkas Trino)
fmt | render | check rapikan / render / cek sinkron
apply [--no-reload] | reload render (+ reload OPA)
GRUP & USER LDAP (sumber grup Trino & StarRocks):
group list [--members] daftar grup (+ anggota)
group add <cn> --member <uid> buat grup (butuh >=1 member)
group remove <cn> hapus grup
group rename <lama> <baru> rename LDAP + role OPA + permitted_groups StarRocks
group add-member|remove-member <cn> <uid>
group check cek roles == grup LDAP == permitted_groups
group sync [--apply] selaraskan permitted_groups (+ saran grup hilang)
user list [--service] daftar user LDAP (atau service account)
user add <uid> [--cn <n>] [--mail <m>] [--password <p>] [--in <group>]
user remove <uid> hapus user (ikut keluar dari semua grup)
Umum: --dry-run (di posisi mana pun) · --env-file <path> · -h/--help
EOF
} }
# ── Ambil --dry-run dari posisi mana pun ───────────────────────────────────── # ── Ekstrak --dry-run dan --env-file dari posisi mana pun ────────────────────
ARGS=() ARGS=(); ENVFILE=""; prev=""
for a in "$@"; do for a in "$@"; do
if [[ "${a}" == "--dry-run" ]]; then if [[ "${prev}" == "--env-file" ]]; then ENVFILE="${a}"; prev=""; continue; fi
export SICABE_AUTHZ_DRY_RUN=1 case "${a}" in
else --dry-run) export SICABE_AUTHZ_DRY_RUN=1 ;;
ARGS+=("${a}") --env-file) prev="--env-file" ;;
fi *) ARGS+=("${a}") ;;
esac
done done
[[ -n "${ENVFILE}" ]] && export SICABE_ENV_FILE="${ENVFILE}"
set -- ${ARGS[@]+"${ARGS[@]}"} set -- ${ARGS[@]+"${ARGS[@]}"}
[[ $# -eq 0 || "${1}" == "-h" || "${1}" == "--help" || "${1}" == "help" ]] && { usage; exit 0; } [[ $# -eq 0 || "${1}" == "-h" || "${1}" == "--help" || "${1}" == "help" ]] && { usage; exit 0; }
PY="${SCRIPT_DIR}/lib/authz.py" # Muat .env (LDAP_* dsb). --env-file sudah diteruskan via SICABE_ENV_FILE.
sicabe_load_env "${SCRIPT_DIR}"
# ── reload: recreate container OPA agar memuat policy+data terbaru ─────────── # ── reload: recreate container OPA agar memuat policy+data terbaru ───────────
reload_opa() { reload_opa() {
+21 -2
View File
@@ -103,8 +103,27 @@ fi
"${CLI}" --dry-run --model "${M}" role add data-ghost >/dev/null 2>&1 "${CLI}" --dry-run --model "${M}" role add data-ghost >/dev/null 2>&1
grep -q 'data-ghost' "${M}" && bad "--dry-run MENULIS berkas" || ok "--dry-run tidak menulis" grep -q 'data-ghost' "${M}" && bad "--dry-run MENULIS berkas" || ok "--dry-run tidak menulis"
# ─── D. Policy OPA (OPA asli via docker) ──────────────────────────────────── # ─── D. Grup/user LDAP (read-only; dilewati bila LDAP tak terjangkau) ────────
echo; echo -e "${BLD}D. Evaluasi policy OPA (docker)${NC}" echo; echo -e "${BLD}D. Grup & user LDAP${NC}"
if python3 - "${DEPLOY_DIR}/lib/authz_ldap.py" <<'PY'
import importlib.util, sys
spec = importlib.util.spec_from_file_location("authz_ldap", sys.argv[1])
m = importlib.util.module_from_spec(spec); spec.loader.exec_module(m)
e = m.parse_ldif("dn: cn=x,dc=a\ncn: x\nmember: uid=1\nmember: uid=2\n\ndn: cn=y,dc=a\ncn: y\n")
assert e[0]["cn"] == "x" and e[0]["member"] == ["uid=1", "uid=2"] and e[1]["cn"] == "y", e
PY
then ok "parser LDIF benar (parse_ldif)"; else bad "parser LDIF salah"; fi
svc_out="$("${CLI}" user list --service 2>/dev/null)"
case "${svc_out}" in *svc_dbt*) ok "user list --service (model) bekerja" ;; *) bad "user list --service keliru" ;; esac
grp_out="$("${CLI}" group list 2>/dev/null)"
if [[ -n "${grp_out}" ]]; then
ok "group list mengembalikan grup dari LDAP (${grp_out//$'\n'/,})"
else
skip "LDAP tak terjangkau — lewati uji grup live"
fi
# ─── E. Policy OPA (OPA asli via docker) ────────────────────────────────────
echo; echo -e "${BLD}E. Evaluasi policy OPA (docker)${NC}"
if docker info >/dev/null 2>&1; then if docker info >/dev/null 2>&1; then
if python3 - "${DEPLOY_DIR}" <<'PY' if python3 - "${DEPLOY_DIR}" <<'PY'
import json, os, shutil, subprocess, sys, tempfile import json, os, shutil, subprocess, sys, tempfile