--- 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')