ui rehaul

This commit is contained in:
Ludwig Lehnert
2026-10-03 09:44:29 +00:00
parent a47542f156
commit 3075d951ba
17 changed files with 428 additions and 102 deletions
+3 -2
View File
@@ -118,6 +118,7 @@ ensure_user eve "$AD_USER_PASSWORD" Eve Evans
ensure_user frank "$AD_USER_PASSWORD" Frank Foster
ensure_user "$AD_WEB_ADMIN_USER" "$AD_WEB_ADMIN_PASSWORD" Preview Administrator
ensure_user report_svc "$AD_USER_PASSWORD" Report Service
ensure_user MSOL_sync "$AD_USER_PASSWORD" Directory Sync
for group in Finance_Analysts Engineering_Leads FS_Finance FS_Engineering FS_Projects; do
ensure_group "$group"
@@ -129,8 +130,8 @@ set_display_name FS_Finance Finance
set_display_name FS_Engineering Engineering
set_display_name FS_Projects Projects
add_members Finance_Analysts alice
add_members FS_Finance 'Finance_Analysts,bob,report_svc'
add_members Finance_Analysts 'alice,MSOL_sync'
add_members FS_Finance 'Finance_Analysts,bob,report_svc,krbtgt'
add_members Engineering_Leads carol
add_members FS_Engineering 'Engineering_Leads,dave'
add_members FS_Projects 'FS_Finance,FS_Engineering,eve'
+62
View File
@@ -289,6 +289,14 @@ def main() -> int:
check({"alice", "bob", "carol", "dave", "eve", "frank"}.issubset({sam for kind, sam, _ in project_nodes if kind == "user"}), "transitive or primary-group Project membership is incomplete")
check(all(kind == "user" for kind, _, _ in project_nodes), "Projects still contains group assignments")
check(not {'msol_sync', 'krbtgt'} & {sam.casefold() for _, sam, _ in finance_nodes + project_nodes},
"service identities were imported as folder users")
service_denied = engine_run(
"exec", CLIENT_CONTAINER, "smbclient", f"//files.{DNS_DOMAIN}/Data", "-m", "SMB3",
"-U", f"{WORKGROUP}\\MSOL_sync%{USER_PASSWORD}", "-c", "cd Finance; ls", check_result=False)
check(service_denied.returncode != 0 or "NT_STATUS_ACCESS_DENIED" in service_denied.stdout,
"excluded service account received migrated access")
announce("legacy GUID-path migration preserves file hashes and inodes")
engine_run("exec", FILES_CONTAINER, "python3", "-c", """
import hashlib, json
@@ -753,6 +761,15 @@ fi
"permissions": [{"kind": "group", "principalId": "legacy", "level": 3}]}, token=token)
check(rejected.status == 400, "group folder assignment accepted")
announce("MSOL_* and krbtgt cannot be selected or granted folder access")
check(not {'msol_sync', 'krbtgt'} & {name.casefold() for name in user_sids}, "excluded accounts appear in access UI data")
for name in ('MSOL_sync', 'krbtgt'):
sid = engine_run("exec", FILES_CONTAINER, "wbinfo", "--name-to-sid", f"{WORKGROUP}\\{name}").stdout.split()[0]
rejected = eventually("excluded-account grant", lambda: http("/api/access", method="POST", value={
'action': 'set-permissions', 'id': folder_id,
'permissions': [{'kind': 'user', 'principalId': sid, 'level': 3}]}, token=token), lambda r: r.status != 409, timeout=45)
check(rejected.status == 400, f"excluded account {name} accepted a direct grant")
def rules(level):
return change({"action": "set-permissions", "id": folder_id,
"permissions": [{"kind": "user", "principalId": user_sids["alice"], "level": level}]})
@@ -816,6 +833,51 @@ fi
invalid = http("/api/access", method="POST", value={"action": "create-folder", "name": "../escape"}, token=token)
check(invalid.status == 400, "unsafe folder path accepted")
announce("previously stored system-account grant revoked without deleting data")
# Reproduce a policy and ACL from a version that allowed MSOL accounts.
previous_service_file = json.loads(engine_run("exec", FILES_CONTAINER, "python3", "-c", """
import hashlib, json, os, sys
sys.path.insert(0, '/app')
import access_control as access
import reconcile_shares as directory
folder_id=sys.argv[1]
os.environ['DOMAIN_ADMINS_SID']=sys.argv[2]+'-512'
sid=directory.run_command(['wbinfo','--name-to-sid','MSOL_sync']).stdout.split()[0]
with access.mutation_lock():
conn=directory.open_db()
row=conn.execute('SELECT * FROM shares WHERE objectGUID=?',(folder_id,)).fetchone()
retained=os.path.join(row['path'],'retained-system-grant.bin')
with open(retained,'xb') as handle:
handle.write(b'previously granted data must survive')
top=access.descriptor({sid:3},os.environ['DOMAIN_ADMINS_SID'],top_level=True)
access.set_acl(row['path'],access.pack_descriptor(top),True)
access.set_acl(retained,access.pack_descriptor(access.descriptor({sid:3},os.environ['DOMAIN_ADMINS_SID'],False)),False)
conn.execute('INSERT INTO folder_permissions VALUES(?,?,?,?)',(folder_id,'user',sid,3))
conn.execute('UPDATE shares SET aclSignature=? WHERE objectGUID=?',(hashlib.sha256(top.encode()).hexdigest(),folder_id))
conn.commit()
conn.close()
with open(retained,'rb') as handle:
print(json.dumps({'sha256':hashlib.sha256(handle.read()).hexdigest(),'inode':os.stat(retained).st_ino}))
""", folder_id, DOMAIN_SID).stdout)
allowed('cd Permissions; get retained-system-grant.bin /tmp/old-system-grant.bin', 'MSOL_sync')
engine_run('exec', FILES_CONTAINER, 'bash', '-lc',
'source /app/runtime.env && exec python3 /app/reconcile_shares.py')
code, output = smb('cd Permissions; get retained-system-grant.bin /tmp/revoked-system-grant.bin', 'MSOL_sync')
check(code != 0 or 'NT_STATUS_ACCESS_DENIED' in output, 'stored system-account grant still permits SMB reads')
actual_service_file = json.loads(engine_run('exec', FILES_CONTAINER, 'python3', '-c', """
import hashlib, json, os, sys
sys.path.insert(0,'/app')
import reconcile_shares as directory
conn=directory.open_db()
assert not conn.execute('SELECT 1 FROM folder_permissions WHERE folderId=?',(sys.argv[1],)).fetchone()
assert not conn.execute("SELECT 1 FROM access_settings WHERE key='pendingExcludedRevocation'").fetchone()
conn.close()
path='/data/groups/data/Permissions/retained-system-grant.bin'
with open(path,'rb') as handle:
print(json.dumps({'sha256':hashlib.sha256(handle.read()).hexdigest(),'inode':os.stat(path).st_ino}))
""", folder_id).stdout)
check(actual_service_file == previous_service_file, 'system-account revocation changed file contents or inode')
announce("already-migrated GUID path survives container restart with all data and rights")
created = change({"action": "create-folder", "name": "Startup recovery"})
repair_id = next(f["id"] for f in created["folders"] if f["name"] == "Startup recovery")