Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
0aa0a9b
feat(startup): bounded one-shot admin sync without blocking Seafile s…
servicememeai Oct 24, 2026
3f54969
feat(startup): call one-shot native admin synchronization after Seafi…
servicememeai Oct 24, 2026
ddeee16
feat(config): generate only two startup admin sync settings, default …
servicememeai Oct 24, 2026
14f871b
fix(config): write actual newline-delimited generated admin settings
servicememeai Oct 24, 2026
5d899e5
feat(compose): opt-in startup admin roster sync on main CloudFile ser…
servicememeai Oct 24, 2026
0abe950
docs(config): document optional startup-only provider admin sync
servicememeai Oct 24, 2026
9aa5250
docs(config): document optional startup-only provider admin sync
servicememeai Oct 24, 2026
336edb4
test(startup): verify one invocation, time bound and nonfatal Directo…
servicememeai Oct 24, 2026
cf07f93
fix(ci): remove extra blank line from Etech environment example
servicememeai Oct 24, 2026
0ddffde
ci(startup): test one-shot sync, timeout and provider-outage isolation
servicememeai Oct 24, 2026
6f207ff
fix(startup): reuse EAP Directory when legacy full SSO sync is disabled
servicememeai Oct 24, 2026
afe662f
fix(admin): expose explicit safe native admin revocation scope
servicememeai Oct 24, 2026
40c3c4e
fix(admin): expose explicit safe native admin revocation scope
servicememeai Oct 24, 2026
4c9d2b3
fix(admin): expose explicit safe native admin revocation scope
servicememeai Oct 24, 2026
032c715
feat(admin): expose optional Etech admin account compatibility check
servicememeai Oct 24, 2026
61d543a
feat(admin): expose optional Etech admin account compatibility check
servicememeai Oct 24, 2026
56790e3
feat(admin): expose optional Etech admin account compatibility check
servicememeai Oct 24, 2026
97fc04f
feat(admin): expose optional Etech admin account compatibility check
servicememeai Oct 24, 2026
25f937f
fix(admin): require explicit applied receipt before reporting startup…
servicememeai Oct 24, 2026
eb15799
test(admin): prevent false-success startup status and keep Etech admi…
servicememeai Oct 24, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/workflows/dev.yml
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,9 @@ jobs:
[[ ! -f "$path" ]] || bash -n "$path"
done < <(git diff --name-only HEAD^ HEAD -- '*.sh')

- name: Validate one-shot startup admin hook (pure Python)
run: python3 -m unittest discover -s tests -p test_admin_startup_hook.py -q

- name: Enforce current V2 ACL documentation contract
run: python3 tests/test_v2_authorization_contract.py

Expand Down
9 changes: 9 additions & 0 deletions deploy/compose/.env.etech-sso.example
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,12 @@
# Generic OAuth defaults stay provider-neutral. No credentials belong in this file.
CF_SSO_OAUTH_LOGIN_ID_CLAIM=userId
CF_SSO_EAP_PROFILE_IDENTITY=false
# Optional: enable ONLY after the EAP Directory /administrators machine endpoint
# and CE admin ownership have been verified. Uses existing Directory URL/key/secret.
# A failed one-time fetch warns without preventing the CloudFile service startup.
CF_PROVIDER_ADMIN_STARTUP_SYNC_ENABLED=false

# Etech's business administrator is `admin`, authenticated through the
# trusted Provider and existing SSO. This requires it in the signed roster;
# it never grants privileges just because the username matches.
CF_PROVIDER_ADMIN_EXPECTED_ACCOUNT=admin
10 changes: 10 additions & 0 deletions deploy/compose/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -344,6 +344,16 @@ CF_SERVICE_SSO_DIRECTORY_SECRET=
CF_SERVICE_SSO_DIRECTORY_AUTH_MODE=v2
CF_SERVICE_SSO_DIRECTORY_KEY_ID=
CF_SERVICE_SSO_DIRECTORY_SERVICE_ID=cloudfile
# Startup-only administrator roster: no per-request Provider checks.
# Enable after verifying the complete machine-authenticated admin list and
# operator ownership of CE default_admin in the enterprise namespace.
CF_PROVIDER_ADMIN_STARTUP_SYNC_ENABLED=false
CF_PROVIDER_ADMIN_SOURCE=
# Optional exact CE usernames owned by Provider for automatic revocation; empty is safest.
CF_PROVIDER_ADMIN_MANAGED_ACCOUNTS=
# Optional expected trusted admin account; not an elevation by username.
CF_PROVIDER_ADMIN_EXPECTED_ACCOUNT=

# UID-scoped incremental administrative sync: safe preview by default.
CF_SSO_UID_DELTA_APPLY_ENABLED=false
CF_SSO_UID_DELTA_MAX_REMOVALS=0
Expand Down
5 changes: 5 additions & 0 deletions deploy/compose/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -301,6 +301,11 @@ services:
CF_SSO_DIRECTORY_STATIC: ${CF_SSO_DIRECTORY_STATIC:-}
CF_SERVICE_SSO_DIRECTORY_URL: ${CF_SERVICE_SSO_DIRECTORY_URL:-}
CF_SERVICE_SSO_DIRECTORY_SECRET: ${CF_SERVICE_SSO_DIRECTORY_SECRET:-}
# One bounded startup-only enterprise admin sync, no periodic/request polling.
CF_PROVIDER_ADMIN_STARTUP_SYNC_ENABLED: ${CF_PROVIDER_ADMIN_STARTUP_SYNC_ENABLED:-false}
CF_PROVIDER_ADMIN_SOURCE: ${CF_PROVIDER_ADMIN_SOURCE:-}
CF_PROVIDER_ADMIN_MANAGED_ACCOUNTS: ${CF_PROVIDER_ADMIN_MANAGED_ACCOUNTS:-}
CF_PROVIDER_ADMIN_EXPECTED_ACCOUNT: ${CF_PROVIDER_ADMIN_EXPECTED_ACCOUNT:-}
# Default to v2 on both app and worker; never auto-downgrade failed auth.
CF_SERVICE_SSO_DIRECTORY_AUTH_MODE: ${CF_SERVICE_SSO_DIRECTORY_AUTH_MODE:-v2}
CF_SERVICE_SSO_DIRECTORY_KEY_ID: ${CF_SERVICE_SSO_DIRECTORY_KEY_ID:-}
Expand Down
7 changes: 7 additions & 0 deletions scripts/scripts_14.0/start.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
from cloudfile import write_settings as write_cloudfile_settings
from cloudfile import write_policy_worker_hooks
from cloudfile_schema import prepare_fresh_schema, initialize_fresh_schema, verify_development_schema
from cloudfile_admin_startup import run_provider_admin_sync


shared_seafiledir = '/shared/seafile'
Expand Down Expand Up @@ -112,6 +113,12 @@ def main():
if exists(password_file):
os.unlink(password_file)

# One low-frequency sync at service start, never in Gunicorn workers or
# every HTTP request. The Provider can be offline; warn and keep CE running.
run_provider_admin_sync(get_script('seahub.sh'),
join(installdir, 'seahub', 'manage.py'),
non_root=non_root)

print('seafile server is running now.')
try:
watch_controller()
Expand Down
44 changes: 44 additions & 0 deletions scripts/scripts_cf14.0/cloudfile_admin_startup.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
"""One bounded CloudFile administrator sync after a normal Seafile startup.

Do not run from Seahub/Gunicorn worker initialization: those launch repeatedly
and must never create another background reconciliation loop.
"""
import os
import shlex
import subprocess


def run_provider_admin_sync(seahub_script, manage_script, *, non_root=False,
enabled=None, runner=subprocess.run):
if enabled is None:
enabled = os.getenv('CF_PROVIDER_ADMIN_STARTUP_SYNC_ENABLED', 'false').lower() == 'true'
if enabled is not True:
return 'SKIPPED'
command = [seahub_script, 'python-env', 'python3', manage_script,
'cf_sync_provider_admins']
if non_root:
command = ['su', '-p', 'seafile', '-c', shlex.join(command)]
try:
# Only one process per container start. Provider request has its own
# 5s bound; leave some time for CE SQL/RPC. Never block the file service
# if an external enterprise Directory is unavailable.
result = runner(command, capture_output=True, text=True, timeout=30,
check=False)
if result.returncode:
print('CF_ADMIN_SYNC FAILED (nonfatal): command returned %d'
% result.returncode, flush=True)
return 'FAILED'
# A zero exit code is not sufficient: the management command may
# explicitly SKIP because its generated settings were not enabled.
# Never claim successful sync without the command's applied marker.
lines = (result.stdout or '').strip().splitlines()
applied = [line.strip() for line in lines
if line.strip().startswith('CF_ADMIN_SYNC APPLIED added=')]
if len(applied) != 1:
print('CF_ADMIN_SYNC FAILED (nonfatal): no applied receipt', flush=True)
return 'FAILED'
print(applied[0], flush=True)
return 'APPLIED'
except (OSError, subprocess.SubprocessError):
print('CF_ADMIN_SYNC FAILED (nonfatal): command unavailable', flush=True)
return 'FAILED'
15 changes: 15 additions & 0 deletions scripts/scripts_cf14.0/cloudfile_bootstrap.py
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,21 @@ def write_cloudfile_settings():
)

body += _settings_block_sso()
# EAP may use the existing OAuth/SSO bridge with the legacy full
# group-synchronizer disabled. When the new one-shot administrator sync is
# explicitly enabled, still expose its trusted Directory connection.
if (not cf_enabled('CF_ENABLE_SSO') and
get_conf('CF_PROVIDER_ADMIN_STARTUP_SYNC_ENABLED', 'false').lower() == 'true'):
body += 'CF_DIRECTORY_URL = %r\n' % get_conf('CF_SERVICE_SSO_DIRECTORY_URL', '')
body += 'CF_DIRECTORY_KEY_ID = %r\n' % get_conf('CF_SERVICE_SSO_DIRECTORY_KEY_ID', '')
body += 'CF_DIRECTORY_SECRET = %r\n' % get_conf('CF_SERVICE_SSO_DIRECTORY_SECRET', '')
# Low-frequency native admin sync is separate from the old SSO worker.
# It must remain off in Standalone CE and must never start an HTTP poll.
body += 'CF_PROVIDER_ADMIN_STARTUP_SYNC_ENABLED = %r\n' % (
get_conf('CF_PROVIDER_ADMIN_STARTUP_SYNC_ENABLED', 'false').lower() == 'true')
body += 'CF_PROVIDER_ADMIN_SOURCE = %r\n' % get_conf('CF_PROVIDER_ADMIN_SOURCE', '')
body += 'CF_PROVIDER_ADMIN_MANAGED_ACCOUNTS = %r\n' % get_conf('CF_PROVIDER_ADMIN_MANAGED_ACCOUNTS', '')
body += 'CF_PROVIDER_ADMIN_EXPECTED_ACCOUNT = %r\n' % get_conf('CF_PROVIDER_ADMIN_EXPECTED_ACCOUNT', '')
body += _settings_block_search()
body += _settings_block_v04_preview_embed()
body += _settings_block_external_sources()
Expand Down
71 changes: 71 additions & 0 deletions tests/test_admin_startup_hook.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
"""Cheap startup-hook checks; no Docker image, database or network required."""
import importlib.util
from pathlib import Path
from types import SimpleNamespace
import subprocess
import unittest
from unittest.mock import Mock

FILE = (Path(__file__).resolve().parents[1] / 'scripts' /
'scripts_cf14.0' / 'cloudfile_admin_startup.py')
spec = importlib.util.spec_from_file_location('admin_sync_startup', FILE)
admin_sync = importlib.util.module_from_spec(spec)
spec.loader.exec_module(admin_sync)


class StartupSyncHookTests(unittest.TestCase):
def test_disabled_never_spawns_an_process(self):
spawn = Mock(side_effect=AssertionError('disabled'))
self.assertEqual(admin_sync.run_provider_admin_sync(
'/seahub.sh', '/seahub/manage.py', enabled=False, runner=spawn),
'SKIPPED')
spawn.assert_not_called()

def test_enabled_spawns_once_with_bounded_timeout(self):
spawn = Mock(return_value=SimpleNamespace(
returncode=0, stdout='CF_ADMIN_SYNC APPLIED added=1 removed=0 unchanged=0\n'))
self.assertEqual(admin_sync.run_provider_admin_sync(
'/seahub.sh', '/seahub/manage.py', enabled=True, runner=spawn), 'APPLIED')
args, kw = spawn.call_args
self.assertEqual(args[0][-2:], ['/seahub/manage.py', 'cf_sync_provider_admins'])
self.assertEqual(kw['timeout'], 30)
self.assertFalse(kw['check'])
self.assertEqual(spawn.call_count, 1)

def test_failed_provider_never_aborts_container_startup(self):
self.assertEqual(admin_sync.run_provider_admin_sync(
'/seahub.sh', '/seahub/manage.py', enabled=True,
runner=Mock(return_value=SimpleNamespace(returncode=1, stdout=''))),
'FAILED')
self.assertEqual(admin_sync.run_provider_admin_sync(
'/seahub.sh', '/seahub/manage.py', enabled=True,
runner=Mock(side_effect=subprocess.TimeoutExpired('sync', 30))),
'FAILED')

def test_zero_exit_without_applied_receipt_is_failure_not_false_success(self):
for stdout in ('', 'CF_ADMIN_SYNC SKIPPED (not configured)',
'unrelated successful output'):
with self.subTest(stdout=stdout):
self.assertEqual(admin_sync.run_provider_admin_sync(
'/seahub.sh', '/seahub/manage.py', enabled=True,
runner=Mock(return_value=SimpleNamespace(
returncode=0, stdout=stdout))), 'FAILED')

def test_etech_default_admin_compatibility_remains_opt_in(self):
root = Path(__file__).resolve().parents[1]
env = (root / 'deploy/compose/.env.etech-sso.example').read_text()
self.assertIn('CF_PROVIDER_ADMIN_EXPECTED_ACCOUNT=admin', env)
self.assertIn('CF_PROVIDER_ADMIN_STARTUP_SYNC_ENABLED=false', env)

def test_nonroot_command_is_one_safely_quoted_shell_command(self):
spawn = Mock(return_value=SimpleNamespace(returncode=0, stdout='CF_ADMIN_SYNC APPLIED added=1 removed=0 unchanged=0'))
self.assertEqual(admin_sync.run_provider_admin_sync(
'/seahub.sh', '/seahub/manage.py', enabled=True,
non_root=True, runner=spawn), 'APPLIED')
args = spawn.call_args.args[0]
self.assertEqual(args[:4], ['su', '-p', 'seafile', '-c'])
self.assertIn('cf_sync_provider_admins', args[-1])


if __name__ == '__main__':
unittest.main()
Loading