96 lines
4 KiB
Diff
96 lines
4 KiB
Diff
|
|
--- a/helm/forgejo-values.yaml
|
||
|
|
+++ b/helm/forgejo-values.yaml
|
||
|
|
@@ -122,6 +122,9 @@
|
||
|
|
DISABLE_SSH: false
|
||
|
|
service:
|
||
|
|
DISABLE_REGISTRATION: true
|
||
|
|
+ oauth2_client:
|
||
|
|
+ ENABLE_AUTO_REGISTRATION: false
|
||
|
|
+ ACCOUNT_LINKING: login
|
||
|
|
security:
|
||
|
|
INSTALL_LOCK: true
|
||
|
|
session:
|
||
|
|
--- /dev/null
|
||
|
|
+++ b/tools/forgejo_chart_projection.py
|
||
|
|
@@ -0,0 +1,80 @@
|
||
|
|
+"""Public source preparation only. No I/O, native tools, credentials or dispatch.
|
||
|
|
+
|
||
|
|
+The actual three-group case is refused on Forgejo 11.0.3. A single-group test
|
||
|
|
+control proves the real chart secret-reference seam, never case admission.
|
||
|
|
+"""
|
||
|
|
+from copy import deepcopy
|
||
|
|
+import hashlib
|
||
|
|
+import re
|
||
|
|
+import yaml
|
||
|
|
+
|
||
|
|
+APPS = '944b30ad63ab054cc24617e374e177d34cd572da'
|
||
|
|
+BASE_SHA256 = 'cf87a3dccd666bf8870619755afac4af64fc0bee77b0f0ea54718d6384b70d4f'
|
||
|
|
+CASE_GROUPS = ('forge-admins', 'forge-coulomb-owners', 'forge-coulomb-readers')
|
||
|
|
+
|
||
|
|
+class Refused(ValueError):
|
||
|
|
+ pass
|
||
|
|
+
|
||
|
|
+
|
||
|
|
+def policy_only(base):
|
||
|
|
+ """Incomplete preparatory policy delta; does not add an OAuth source."""
|
||
|
|
+ result = deepcopy(base)
|
||
|
|
+ config = result['gitea']['config']
|
||
|
|
+ if config.get('service', {}).get('DISABLE_REGISTRATION') is not True:
|
||
|
|
+ raise Refused('existing registration policy differs')
|
||
|
|
+ # No supported local-form switch in 11.0.3: retain existing settings.
|
||
|
|
+ oauth = config.setdefault('oauth2_client', {})
|
||
|
|
+ oauth.update(ENABLE_AUTO_REGISTRATION=False, ACCOUNT_LINKING='login')
|
||
|
|
+ return result
|
||
|
|
+
|
||
|
|
+
|
||
|
|
+def chart_projection(base, groups):
|
||
|
|
+ """Chart-compatible PUBLIC values for one literal gate, or fail closed.
|
||
|
|
+
|
||
|
|
+ groups must be the authoritative reviewed case set, never a selected subset.
|
||
|
|
+ This function authenticates neither case/source acceptance nor permission.
|
||
|
|
+ It is not suitable for rendering arbitrary live/private Helm values.
|
||
|
|
+ """
|
||
|
|
+ if not isinstance(groups, tuple) or not groups or len(set(groups)) != len(groups):
|
||
|
|
+ raise Refused('explicit unique group tuple required')
|
||
|
|
+ if any(not isinstance(g, str) or not re.fullmatch(r'[a-z][a-z0-9-]{1,62}', g) for g in groups):
|
||
|
|
+ raise Refused('group literal invalid')
|
||
|
|
+ if len(groups) != 1:
|
||
|
|
+ raise Refused('Forgejo 11.0.3 supports one literal required claim; any-of unavailable')
|
||
|
|
+ result = policy_only(base)
|
||
|
|
+ sources = result['gitea'].setdefault('oauth', [])
|
||
|
|
+ if not isinstance(sources, list) or any(not isinstance(s, dict) for s in sources):
|
||
|
|
+ raise Refused('OAuth source list required')
|
||
|
|
+ names = [s.get('name') for s in sources]
|
||
|
|
+ if len(names) != len(set(names)) or 'netkingdom' in names:
|
||
|
|
+ raise Refused('existing target requires exact owner reconciliation')
|
||
|
|
+ idx = len(sources)
|
||
|
|
+ env = result.setdefault('deployment', {}).setdefault('env', [])
|
||
|
|
+ if not isinstance(env, list):
|
||
|
|
+ raise Refused('deployment env list required')
|
||
|
|
+ var = f'GITEA_OAUTH_SECRET_{idx}'
|
||
|
|
+ if any(e.get('name') == var for e in env):
|
||
|
|
+ raise Refused('indexed secret environment collision')
|
||
|
|
+ sources.append({
|
||
|
|
+ 'name': 'netkingdom', 'provider': 'openidConnect', 'key': 'forgejo',
|
||
|
|
+ 'autoDiscoverUrl': 'https://kc.coulomb.social/.well-known/openid-configuration',
|
||
|
|
+ 'scopes': 'openid,profile,email,groups', 'groupClaimName': 'groups',
|
||
|
|
+ 'requiredClaimName': 'groups', 'requiredClaimValue': groups[0],
|
||
|
|
+ })
|
||
|
|
+ env.append({'name': var, 'valueFrom': {'secretKeyRef': {
|
||
|
|
+ 'name': 'forgejo-netkingdom-oidc-client', 'key': 'CLIENT_SECRET'}}})
|
||
|
|
+ return result
|
||
|
|
+
|
||
|
|
+
|
||
|
|
+def prepare_first_case(base_bytes, base):
|
||
|
|
+ """Pin the public baseline and keep exact case any-of semantics."""
|
||
|
|
+ if hashlib.sha256(base_bytes).hexdigest() != BASE_SHA256:
|
||
|
|
+ raise Refused('frozen Apps public values bytes differ')
|
||
|
|
+ if base != yaml.safe_load(base_bytes):
|
||
|
|
+ raise Refused('decoded public baseline differs from frozen bytes')
|
||
|
|
+ # This entry never returns while the actual case has an unsupported gate.
|
||
|
|
+ return chart_projection(base, CASE_GROUPS)
|
||
|
|
+
|
||
|
|
+
|
||
|
|
+def activate(*args, **kwargs):
|
||
|
|
+ raise Refused('source preparation has no operational backend or admission')
|