Org-Ressourcen: Tabellen, API und Auftrag org-ressourcen im Check-in (Migration 0062)
Ressourcen (Mail, Nextcloud, ownCloud, WebDAV, SMB, Web-Proxy) gehoeren zur Organisation und wirken fuer die ganze Org, OEs samt Unter-OEs oder Geraetegruppen. Keine Zugangsdaten: unbekannte Felder werden verworfen. Der Check-in schickt die fuer das Geraet geltenden Ressourcen (spezifisch zuerst) als Auftrag org-ressourcen, vorerst nur fuer Organisationen mit Ausrollstufe vorab (ORG_RESSOURCEN_NUR_VORAB). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
413d9a4795
commit
ed145be3d2
2 files changed
+486
-3
No files matched your search
@@ -394,6 +394,15 @@ class CreateGruppeRequest(BaseModel):
|
||||
name: str
|
||||
|
||||
|
||||
class OrgRessourceRequest(BaseModel):
|
||||
typ: str
|
||||
name: str
|
||||
daten: dict = {}
|
||||
ganze_organisation: bool = True
|
||||
oe_ids: list[str] = []
|
||||
gruppe_ids: list[str] = []
|
||||
|
||||
|
||||
class UpdateGruppeRequest(BaseModel):
|
||||
name: str
|
||||
|
||||
@@ -1576,6 +1585,8 @@ def fetch_auftragskatalog_state(device_id: str):
|
||||
standardbrowser_row = cur.fetchone()
|
||||
standardbrowser = standardbrowser_row[0] if standardbrowser_row else None
|
||||
|
||||
ressourcen = fetch_geraete_ressourcen(cur, device_id)
|
||||
|
||||
overrides = resolve_katalog_overrides(device_id)
|
||||
|
||||
ergebnis = []
|
||||
@@ -1603,8 +1614,367 @@ def fetch_auftragskatalog_state(device_id: str):
|
||||
"optionen": {"browser": standardbrowser} if standardbrowser else {},
|
||||
})
|
||||
|
||||
# Org-Ressourcen (siehe ORG_RESSOURCEN_AUFTRAG): wie beim Standardbrowser
|
||||
# immer mitschicken, auch als "absent", damit die Rolle nach dem Löschen
|
||||
# der letzten Ressource aufräumt. Nicht freigeschaltet: gar nicht.
|
||||
if ressourcen is not None:
|
||||
ergebnis.append({
|
||||
"merkmal": ORG_RESSOURCEN_AUFTRAG,
|
||||
"ansible_role": ORG_RESSOURCEN_AUFTRAG,
|
||||
"state": "present" if ressourcen else "absent",
|
||||
"optionen": {"ressourcen": ressourcen} if ressourcen else {},
|
||||
})
|
||||
|
||||
return ergebnis
|
||||
|
||||
# Org-Ressourcen (15.10.2026, platform-docs roadmap/org-ressourcen-konzept.md,
|
||||
# Migration 0062): lokale Dienste einer Organisation, die App-Rollen zum
|
||||
# Vorbefüllen nutzen. Der Check-in schickt die für ein Gerät geltenden
|
||||
# Ressourcen als eigenen Auftrag "org-ressourcen" mit (wie den
|
||||
# Standardbrowser: kein Katalog-Merkmal, keine Ereigniszeile, der Agent
|
||||
# reicht ihn ungesehen an Ansible weiter). App-Rollen lesen sie im selben
|
||||
# Lauf aus tuxflotte_auftrag_optionen['org-ressourcen'].
|
||||
ORG_RESSOURCEN_AUFTRAG = "org-ressourcen"
|
||||
|
||||
# Freischaltung: solange noch keine App-Rolle die Ressourcen nutzt, gelten
|
||||
# Auftrag und Kunden-Maske nur für Organisationen mit Ausrollstufe "vorab"
|
||||
# (Testorganisationen). Sonst bekäme jedes Kundengerät einen zusätzlichen
|
||||
# Ansible-Lauf für eine Rolle, die sein stabiler Stand noch nicht kennt, und
|
||||
# Kunden eine Maske ohne Wirkung. Auf False stellen, sobald die erste
|
||||
# nutzende Rolle in ansible-content-stabil freigegeben ist.
|
||||
ORG_RESSOURCEN_NUR_VORAB = True
|
||||
|
||||
# Feldtypen: "text", "url", "port", "auswahl" (mit "werte"), "liste"
|
||||
# (kommagetrennt). Pflichtfelder mit "pflicht". Felder für Zugangsdaten gibt
|
||||
# es absichtlich nicht - die Ressourcen landen unverschlüsselt in
|
||||
# /etc/tuxflotte/org-ressourcen.json und sind für jeden Benutzer lesbar.
|
||||
ORG_RESSOURCEN_TYPEN = {
|
||||
"mail": {
|
||||
"name": "E-Mail (IMAP/SMTP)",
|
||||
"felder": [
|
||||
{"key": "mail_domain", "label": "Maildomäne (Teil nach dem @, z. B. schule.de)", "typ": "text", "pflicht": True},
|
||||
{"key": "imap_server", "label": "IMAP-Server", "typ": "text", "pflicht": True},
|
||||
{"key": "imap_port", "label": "IMAP-Port", "typ": "port", "standard": 993},
|
||||
{"key": "imap_verschluesselung", "label": "IMAP-Verschlüsselung", "typ": "auswahl", "werte": ["ssl", "starttls"], "standard": "ssl"},
|
||||
{"key": "smtp_server", "label": "SMTP-Server", "typ": "text", "pflicht": True},
|
||||
{"key": "smtp_port", "label": "SMTP-Port", "typ": "port", "standard": 465},
|
||||
{"key": "smtp_verschluesselung", "label": "SMTP-Verschlüsselung", "typ": "auswahl", "werte": ["ssl", "starttls"], "standard": "ssl"},
|
||||
{"key": "benutzername", "label": "Anmeldename", "typ": "auswahl", "werte": ["mailadresse", "lokalteil"], "standard": "mailadresse"},
|
||||
],
|
||||
},
|
||||
"nextcloud": {
|
||||
"name": "Nextcloud",
|
||||
"felder": [
|
||||
{"key": "url", "label": "Adresse (z. B. https://cloud.schule.de)", "typ": "url", "pflicht": True},
|
||||
],
|
||||
},
|
||||
"owncloud": {
|
||||
"name": "ownCloud",
|
||||
"felder": [
|
||||
{"key": "url", "label": "Adresse (z. B. https://cloud.schule.de)", "typ": "url", "pflicht": True},
|
||||
],
|
||||
},
|
||||
"webdav": {
|
||||
"name": "WebDAV-Freigabe",
|
||||
"felder": [
|
||||
{"key": "url", "label": "Adresse (z. B. https://server/remote.php/dav/files/)", "typ": "url", "pflicht": True},
|
||||
],
|
||||
},
|
||||
"smb": {
|
||||
"name": "Windows-Freigabe (SMB/CIFS)",
|
||||
"felder": [
|
||||
{"key": "server", "label": "Server (Name oder IP)", "typ": "text", "pflicht": True},
|
||||
{"key": "freigabe", "label": "Freigabename", "typ": "text", "pflicht": True},
|
||||
{"key": "domaene", "label": "Domäne/Arbeitsgruppe (optional)", "typ": "text"},
|
||||
],
|
||||
},
|
||||
"proxy": {
|
||||
"name": "Web-Proxy",
|
||||
"felder": [
|
||||
{"key": "server", "label": "Proxy-Server (Name oder IP)", "typ": "text", "pflicht": True},
|
||||
{"key": "port", "label": "Port", "typ": "port", "standard": 3128},
|
||||
{"key": "ausnahmen", "label": "Ohne Proxy (kommagetrennt, z. B. localhost,.schule.local)", "typ": "liste"},
|
||||
],
|
||||
},
|
||||
}
|
||||
|
||||
# Name/Host: Buchstaben, Ziffern, Punkt, Bindestrich, Unterstrich (IPv6 in
|
||||
# eckigen Klammern). Freigabename zusätzlich Leerzeichen und $.
|
||||
_RESSOURCE_HOST_RE = re.compile(r"^(\[[0-9a-fA-F:]+\]|[A-Za-z0-9._-]+)$")
|
||||
_RESSOURCE_FREIGABE_RE = re.compile(r"^[A-Za-z0-9 ._$-]+$")
|
||||
|
||||
|
||||
def pruefe_ressource(typ: str, daten: dict) -> tuple[dict | None, str | None]:
|
||||
"""Prüft die Felder einer Ressource gegen ORG_RESSOURCEN_TYPEN. Liefert
|
||||
(bereinigte Daten, None) oder (None, Fehlermeldung). Unbekannte Felder
|
||||
werden verworfen, damit auch keine Zugangsdaten hineinrutschen."""
|
||||
|
||||
definition = ORG_RESSOURCEN_TYPEN.get(typ)
|
||||
if definition is None:
|
||||
return None, f"Unbekannter Ressourcentyp: {typ}"
|
||||
|
||||
ergebnis = {}
|
||||
for feld in definition["felder"]:
|
||||
roh = daten.get(feld["key"])
|
||||
wert = roh.strip() if isinstance(roh, str) else roh
|
||||
if wert in (None, ""):
|
||||
if feld.get("pflicht"):
|
||||
return None, f"Bitte „{feld['label']}“ ausfüllen."
|
||||
if "standard" in feld:
|
||||
ergebnis[feld["key"]] = feld["standard"]
|
||||
continue
|
||||
|
||||
if feld["typ"] == "port":
|
||||
try:
|
||||
wert = int(wert)
|
||||
except (TypeError, ValueError):
|
||||
return None, f"„{feld['label']}“ muss eine Zahl sein."
|
||||
if not 1 <= wert <= 65535:
|
||||
return None, f"„{feld['label']}“ muss zwischen 1 und 65535 liegen."
|
||||
elif feld["typ"] == "auswahl":
|
||||
if wert not in feld["werte"]:
|
||||
return None, f"Ungültiger Wert für „{feld['label']}“."
|
||||
elif feld["typ"] == "url":
|
||||
teile = urlparse(str(wert))
|
||||
if teile.scheme not in ("http", "https") or not teile.hostname:
|
||||
return None, f"„{feld['label']}“ muss mit http:// oder https:// beginnen."
|
||||
if teile.username or teile.password:
|
||||
return None, "Zugangsdaten gehören nicht in die Adresse."
|
||||
wert = str(wert).rstrip("/")
|
||||
elif feld["typ"] == "liste":
|
||||
eintraege = [e.strip() for e in str(wert).split(",") if e.strip()]
|
||||
if any(not _RESSOURCE_HOST_RE.match(e.lstrip("*")) for e in eintraege):
|
||||
return None, f"„{feld['label']}“ enthält ungültige Einträge."
|
||||
wert = eintraege
|
||||
else:
|
||||
wert = str(wert)
|
||||
muster = _RESSOURCE_FREIGABE_RE if feld["key"] == "freigabe" else _RESSOURCE_HOST_RE
|
||||
if feld["key"] in ("mail_domain", "imap_server", "smtp_server", "server", "freigabe", "domaene") and not muster.match(wert):
|
||||
return None, f"„{feld['label']}“ enthält ungültige Zeichen."
|
||||
|
||||
ergebnis[feld["key"]] = wert
|
||||
|
||||
return ergebnis, None
|
||||
|
||||
|
||||
def _ist_uuid(wert) -> bool:
|
||||
try:
|
||||
UUID(str(wert))
|
||||
except ValueError:
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def org_ressourcen_freigeschaltet(cur, organization_id) -> bool:
|
||||
if not ORG_RESSOURCEN_NUR_VORAB:
|
||||
return True
|
||||
cur.execute("SELECT ausrollstufe FROM organizations WHERE id = %s", (organization_id,))
|
||||
row = cur.fetchone()
|
||||
return bool(row) and row[0] == "vorab"
|
||||
|
||||
|
||||
def _ressourcen_ziele_pruefen(cur, organization_id, oe_ids, gruppe_ids) -> str | None:
|
||||
if oe_ids:
|
||||
cur.execute(
|
||||
"SELECT count(*) FROM organisationseinheiten WHERE organization_id = %s AND id::text = ANY(%s)",
|
||||
(organization_id, oe_ids),
|
||||
)
|
||||
if cur.fetchone()[0] != len(set(oe_ids)):
|
||||
return "Unbekannte Organisationseinheit."
|
||||
if gruppe_ids:
|
||||
cur.execute(
|
||||
"SELECT count(*) FROM gruppen WHERE organization_id = %s AND id::text = ANY(%s)",
|
||||
(organization_id, gruppe_ids),
|
||||
)
|
||||
if cur.fetchone()[0] != len(set(gruppe_ids)):
|
||||
return "Unbekannte Gruppe."
|
||||
return None
|
||||
|
||||
|
||||
def fetch_org_ressourcen(organization_id: str) -> dict:
|
||||
if not _ist_uuid(organization_id):
|
||||
return {"freigeschaltet": False, "ressourcen": []}
|
||||
with get_database_connection() as conn:
|
||||
with conn.cursor() as cur:
|
||||
freigeschaltet = org_ressourcen_freigeschaltet(cur, organization_id)
|
||||
cur.execute(
|
||||
"""
|
||||
SELECT id, typ, name, daten, ganze_organisation
|
||||
FROM org_ressourcen WHERE organization_id = %s
|
||||
ORDER BY typ, name
|
||||
""",
|
||||
(organization_id,),
|
||||
)
|
||||
zeilen = cur.fetchall()
|
||||
cur.execute(
|
||||
"""
|
||||
SELECT z.ressource_id, z.oe_id, oe.name, z.gruppe_id, g.name
|
||||
FROM org_ressourcen_ziele z
|
||||
JOIN org_ressourcen r ON r.id = z.ressource_id
|
||||
LEFT JOIN organisationseinheiten oe ON oe.id = z.oe_id
|
||||
LEFT JOIN gruppen g ON g.id = z.gruppe_id
|
||||
WHERE r.organization_id = %s
|
||||
ORDER BY oe.name, g.name
|
||||
""",
|
||||
(organization_id,),
|
||||
)
|
||||
ziele = {}
|
||||
for ressource_id, oe_id, oe_name, gruppe_id, gruppe_name in cur.fetchall():
|
||||
eintrag = ziele.setdefault(ressource_id, {"oes": [], "gruppen": []})
|
||||
if oe_id is not None:
|
||||
eintrag["oes"].append({"id": str(oe_id), "name": oe_name})
|
||||
else:
|
||||
eintrag["gruppen"].append({"id": str(gruppe_id), "name": gruppe_name})
|
||||
|
||||
ressourcen = []
|
||||
for ressource_id, typ, name, daten, ganze_organisation in zeilen:
|
||||
z = ziele.get(ressource_id, {"oes": [], "gruppen": []})
|
||||
ressourcen.append({
|
||||
"id": str(ressource_id),
|
||||
"typ": typ,
|
||||
"typ_name": ORG_RESSOURCEN_TYPEN.get(typ, {}).get("name", typ),
|
||||
"name": name,
|
||||
"daten": daten,
|
||||
"ganze_organisation": ganze_organisation,
|
||||
"oes": z["oes"],
|
||||
"gruppen": z["gruppen"],
|
||||
})
|
||||
|
||||
return {"freigeschaltet": freigeschaltet, "ressourcen": ressourcen}
|
||||
|
||||
|
||||
def speichere_org_ressource(
|
||||
organization_id: str, ressource_id: str | None, typ: str, name: str, daten: dict,
|
||||
ganze_organisation: bool, oe_ids: list[str], gruppe_ids: list[str],
|
||||
) -> tuple[dict | None, str | None]:
|
||||
"""Legt eine Ressource an (ressource_id None) oder ersetzt sie komplett
|
||||
(Felder und Ziele). Liefert ({"id": …}, None) oder (None, Fehler)."""
|
||||
|
||||
if not all(_ist_uuid(x) for x in [organization_id, *([ressource_id] if ressource_id else []), *(oe_ids or []), *(gruppe_ids or [])]):
|
||||
return None, "Ungültige Kennung."
|
||||
name = (name or "").strip()
|
||||
if not name:
|
||||
return None, "Bitte einen Namen angeben."
|
||||
bereinigt, fehler = pruefe_ressource(typ, daten or {})
|
||||
if fehler:
|
||||
return None, fehler
|
||||
oe_ids = [] if ganze_organisation else list(dict.fromkeys(oe_ids or []))
|
||||
gruppe_ids = [] if ganze_organisation else list(dict.fromkeys(gruppe_ids or []))
|
||||
|
||||
with get_database_connection() as conn:
|
||||
with conn.cursor() as cur:
|
||||
if fehler := _ressourcen_ziele_pruefen(cur, organization_id, oe_ids, gruppe_ids):
|
||||
return None, fehler
|
||||
try:
|
||||
if ressource_id is None:
|
||||
ressource_id = str(uuid4())
|
||||
cur.execute(
|
||||
"""
|
||||
INSERT INTO org_ressourcen (id, organization_id, typ, name, daten, ganze_organisation)
|
||||
VALUES (%s, %s, %s, %s, %s, %s)
|
||||
""",
|
||||
(ressource_id, organization_id, typ, name, Jsonb(bereinigt), ganze_organisation),
|
||||
)
|
||||
else:
|
||||
cur.execute(
|
||||
"""
|
||||
UPDATE org_ressourcen
|
||||
SET typ = %s, name = %s, daten = %s, ganze_organisation = %s, updated_at = now()
|
||||
WHERE id = %s AND organization_id = %s
|
||||
""",
|
||||
(typ, name, Jsonb(bereinigt), ganze_organisation, ressource_id, organization_id),
|
||||
)
|
||||
if cur.rowcount == 0:
|
||||
return None, "Die Ressource wurde nicht gefunden."
|
||||
cur.execute("DELETE FROM org_ressourcen_ziele WHERE ressource_id = %s", (ressource_id,))
|
||||
except psycopg.errors.UniqueViolation:
|
||||
conn.rollback()
|
||||
return None, "Eine Ressource mit diesem Namen gibt es schon."
|
||||
|
||||
for oe_id in oe_ids:
|
||||
cur.execute("INSERT INTO org_ressourcen_ziele (ressource_id, oe_id) VALUES (%s, %s)", (ressource_id, oe_id))
|
||||
for gruppe_id in gruppe_ids:
|
||||
cur.execute("INSERT INTO org_ressourcen_ziele (ressource_id, gruppe_id) VALUES (%s, %s)", (ressource_id, gruppe_id))
|
||||
|
||||
return {"id": ressource_id}, None
|
||||
|
||||
|
||||
def loesche_org_ressource(organization_id: str, ressource_id: str) -> bool:
|
||||
if not (_ist_uuid(organization_id) and _ist_uuid(ressource_id)):
|
||||
return False
|
||||
with get_database_connection() as conn:
|
||||
with conn.cursor() as cur:
|
||||
cur.execute(
|
||||
"DELETE FROM org_ressourcen WHERE id = %s AND organization_id = %s",
|
||||
(ressource_id, organization_id),
|
||||
)
|
||||
return cur.rowcount > 0
|
||||
|
||||
|
||||
def fetch_geraete_ressourcen(cur, device_id) -> list[dict] | None:
|
||||
"""Ressourcen, die für ein Gerät gelten - None, wenn sie für seine
|
||||
Organisation (noch) nicht freigeschaltet sind. Eine Ressource gilt, wenn
|
||||
sie für die ganze Organisation wirkt, eine ihrer OEs die OE des Geräts
|
||||
oder eine darüberliegende ist, oder das Gerät in einer ihrer Gruppen ist.
|
||||
|
||||
Sortiert vom Spezifischsten zum Allgemeinsten (Gruppe, OE nahe am Gerät,
|
||||
OE weiter oben, ganze Organisation), innerhalb gleicher Stufe nach Name:
|
||||
Rollen, die nur eine Ressource eines Typs brauchen (Web-Proxy), nehmen
|
||||
die erste."""
|
||||
|
||||
cur.execute("SELECT organization_id, oe_id FROM devices WHERE id = %s", (device_id,))
|
||||
row = cur.fetchone()
|
||||
if row is None:
|
||||
return None
|
||||
organization_id, oe_id = row
|
||||
if not org_ressourcen_freigeschaltet(cur, organization_id):
|
||||
return None
|
||||
|
||||
# Abstand jeder OE der Kette zur Geräte-OE (0 = eigene OE), wie in
|
||||
# resolve_katalog_overrides() in Python verfolgt.
|
||||
abstand_by_oe = {}
|
||||
if oe_id is not None:
|
||||
cur.execute("SELECT id, parent_id FROM organisationseinheiten WHERE organization_id = %s", (organization_id,))
|
||||
parent_by_id = {r[0]: r[1] for r in cur.fetchall()}
|
||||
current, abstand = oe_id, 0
|
||||
while current is not None and current not in abstand_by_oe:
|
||||
abstand_by_oe[current] = abstand
|
||||
current, abstand = parent_by_id.get(current), abstand + 1
|
||||
|
||||
cur.execute("SELECT gruppe_id FROM device_gruppen WHERE device_id = %s", (device_id,))
|
||||
gruppen = {r[0] for r in cur.fetchall()}
|
||||
|
||||
cur.execute(
|
||||
"""
|
||||
SELECT r.id, r.typ, r.name, r.daten, r.ganze_organisation, z.oe_id, z.gruppe_id
|
||||
FROM org_ressourcen r
|
||||
LEFT JOIN org_ressourcen_ziele z ON z.ressource_id = r.id
|
||||
WHERE r.organization_id = %s
|
||||
""",
|
||||
(organization_id,),
|
||||
)
|
||||
|
||||
# Rang je Ressource: kleiner = spezifischer. Gruppe 0, OE 1 + Abstand,
|
||||
# ganze Organisation 1000.
|
||||
beste = {}
|
||||
for ressource_id, typ, name, daten, ganze_organisation, z_oe, z_gruppe in cur.fetchall():
|
||||
kandidaten = []
|
||||
if ganze_organisation:
|
||||
kandidaten.append((1000, "organisation"))
|
||||
if z_gruppe is not None and z_gruppe in gruppen:
|
||||
kandidaten.append((0, "gruppe"))
|
||||
if z_oe is not None and z_oe in abstand_by_oe:
|
||||
kandidaten.append((1 + abstand_by_oe[z_oe], "oe"))
|
||||
for rang, quelle in kandidaten:
|
||||
if ressource_id not in beste or rang < beste[ressource_id][0]:
|
||||
beste[ressource_id] = (rang, quelle, typ, name, daten)
|
||||
|
||||
return [
|
||||
{"id": str(rid), "typ": typ, "name": name, "quelle": quelle, **daten}
|
||||
for rid, (rang, quelle, typ, name, daten) in sorted(beste.items(), key=lambda e: (e[1][0], e[1][3]))
|
||||
]
|
||||
|
||||
|
||||
def fetch_auftragskatalog_listing(device_id: str):
|
||||
"""
|
||||
Wie fetch_auftragskatalog_state, aber für die Auswahl-API angereichert um
|
||||
@@ -5770,9 +6140,13 @@ def agent_report(
|
||||
with conn.cursor() as cur:
|
||||
merkmal_ids = {}
|
||||
|
||||
# Der Standardbrowser-Auftrag ist kein Merkmal und hat keine
|
||||
# Ereigniszeile (siehe STANDARDBROWSER_AUFTRAG).
|
||||
payload.results = [e for e in payload.results if e.merkmal != STANDARDBROWSER_AUFTRAG]
|
||||
# Standardbrowser- und Org-Ressourcen-Auftrag sind keine Merkmale
|
||||
# und haben keine Ereigniszeile (siehe STANDARDBROWSER_AUFTRAG,
|
||||
# ORG_RESSOURCEN_AUFTRAG).
|
||||
payload.results = [
|
||||
e for e in payload.results
|
||||
if e.merkmal not in (STANDARDBROWSER_AUFTRAG, ORG_RESSOURCEN_AUFTRAG)
|
||||
]
|
||||
|
||||
for entry in payload.results:
|
||||
if entry.merkmal in merkmal_ids:
|
||||
@@ -7245,6 +7619,66 @@ def list_gruppen(organization_id: str, authorization: str | None = Header(defaul
|
||||
return {"success": True, "gruppen": fetch_gruppen(organization_id)}
|
||||
|
||||
|
||||
@app.get("/api/v1/org-ressourcen/typen")
|
||||
def list_org_ressourcen_typen(authorization: str | None = Header(default=None)):
|
||||
if not require_service_token(authorization):
|
||||
return {"success": False, "error": "unauthorized", "message": "Fehlendes oder ungültiges Service-Token."}
|
||||
|
||||
return {"success": True, "typen": ORG_RESSOURCEN_TYPEN}
|
||||
|
||||
|
||||
@app.get("/api/v1/organizations/{organization_id}/ressourcen")
|
||||
def list_org_ressourcen(organization_id: str, authorization: str | None = Header(default=None)):
|
||||
if not require_service_token(authorization):
|
||||
return {"success": False, "error": "unauthorized", "message": "Fehlendes oder ungültiges Service-Token."}
|
||||
|
||||
return {"success": True, **fetch_org_ressourcen(organization_id)}
|
||||
|
||||
|
||||
@app.post("/api/v1/organizations/{organization_id}/ressourcen")
|
||||
def create_org_ressource(organization_id: str, payload: OrgRessourceRequest, authorization: str | None = Header(default=None)):
|
||||
if not require_service_token(authorization):
|
||||
return {"success": False, "error": "unauthorized", "message": "Fehlendes oder ungültiges Service-Token."}
|
||||
|
||||
ergebnis, fehler = speichere_org_ressource(
|
||||
organization_id, None, payload.typ, payload.name, payload.daten,
|
||||
payload.ganze_organisation, payload.oe_ids, payload.gruppe_ids,
|
||||
)
|
||||
if fehler:
|
||||
return {"success": False, "error": "invalid", "message": fehler}
|
||||
|
||||
return {"success": True, "ressource": ergebnis}
|
||||
|
||||
|
||||
@app.put("/api/v1/organizations/{organization_id}/ressourcen/{ressource_id}")
|
||||
def update_org_ressource(
|
||||
organization_id: str, ressource_id: str, payload: OrgRessourceRequest,
|
||||
authorization: str | None = Header(default=None),
|
||||
):
|
||||
if not require_service_token(authorization):
|
||||
return {"success": False, "error": "unauthorized", "message": "Fehlendes oder ungültiges Service-Token."}
|
||||
|
||||
ergebnis, fehler = speichere_org_ressource(
|
||||
organization_id, ressource_id, payload.typ, payload.name, payload.daten,
|
||||
payload.ganze_organisation, payload.oe_ids, payload.gruppe_ids,
|
||||
)
|
||||
if fehler:
|
||||
return {"success": False, "error": "invalid", "message": fehler}
|
||||
|
||||
return {"success": True, "ressource": ergebnis}
|
||||
|
||||
|
||||
@app.delete("/api/v1/organizations/{organization_id}/ressourcen/{ressource_id}")
|
||||
def delete_org_ressource(organization_id: str, ressource_id: str, authorization: str | None = Header(default=None)):
|
||||
if not require_service_token(authorization):
|
||||
return {"success": False, "error": "unauthorized", "message": "Fehlendes oder ungültiges Service-Token."}
|
||||
|
||||
if not loesche_org_ressource(organization_id, ressource_id):
|
||||
return {"success": False, "error": "not_found", "message": "Die Ressource wurde nicht gefunden."}
|
||||
|
||||
return {"success": True}
|
||||
|
||||
|
||||
@app.post("/api/v1/organizations/{organization_id}/gruppen")
|
||||
def create_gruppe_endpoint(organization_id: str, payload: CreateGruppeRequest, authorization: str | None = Header(default=None)):
|
||||
if not require_service_token(authorization):
|
||||
|
||||
Reference in new issue
Block a user