# Project member repository permissions — #1001 On 3 November 2026, an ordinary Waldur Project member created repositories through Themelio and pushed to protected `main` in both an existing Project and a freshly provisioned Project. Unrelated users were refused. Removing the membership removed repository creation, reads and Git writes. PR #1009 is stacked on #995 at `cf56da806553edc52957f8d78377a7fe028c4149`. Application revision `8b9dc4833de3701c67f0ac111b1971a193facd2e` was deployed to ARM64 `dev-vasilis-m3` through `builder-m3`. The provider was GitLab 19.5.1. All 13 Argo Applications were Healthy and Synced at that revision. The Metallo image digest was `sha256:59c6d7b23ebee839820b10948a848291dfe6cce50993a72f56738e767a4b0f1b`. Subsequent changes correct the rename qualification fixture and document these results; they do not change application code. ## Policy and boundaries Source Control maps the two supported Beto roles, `developer` and `project-administrator`, to GitLab Maintainer. Unknown roles grant nothing. Waldur roles and Beto's membership administration permissions stay unchanged. Run credentials retain Developer access. See the [owner rules](../../services/metallo/contexts/source_control/README.md#authority-freshness-and-revocation). Existing groups upgrade through reconciliation with fresh authority. Expired authority retains the last applied access level, including Developer grants made under the previous policy. The ceiling is independent of role translation, so changing the role map cannot upgrade stale authority. The live journey used three disposable identities: a Project manager, an ordinary member and an unrelated user. They signed in through the real AAI and completed provider preparation and AUP. Administrative setup created two Waldur Projects and assigned `PROJECT.MANAGER` and `PROJECT.MEMBER` through Waldur's API implementation. The deployed Beto and Source Control processes acquired and reconciled those memberships. Browser requests and Git operations used the ordinary users' credentials. ## Live acceptance The final browser/Git journey passed all 29 assertions. | Check | Result | | --- | --- | | Existing Project before deployment | The ordinary member held GitLab Developer, level 30. | | Existing Project after deployment | The same group, ID 309, gave the member Maintainer, level 40. | | Fresh Project | Source Control provisioned the group and gave the member Maintainer. | | Canonical roles | Both Projects continued to report `developer` for the ordinary member. | | Repository creation | The member submitted the Themelio model form in both Projects; both requests returned HTTP 200 and opened the repository page. | | Git push | The member pushed a new commit to `main` in both repositories; remote refs matched the local commit hashes. | | Branch protection | Both `main` branches remained protected with Maintainer push access. | | Unrelated user | Repository creation, repository reads and authenticated Git reads were refused in both Projects. | | Membership removal | Canonical Project reads and repository creation returned HTTP 404; Git reads and fast-forward push attempts were refused. | Fixture identity and successful commits: | Project | Canonical ID | Successful `main` commit | | --- | --- | --- | | Existing | `b898cea0-3ebc-43bd-8126-2c62bf595db1` | `e00ff857b5c9ccb5c18e433b77ec99d9b74c493a` | | Fresh | `d7a55f72-2b0a-4097-b0cb-cfd5b5d3bd31` | `b55158ccac6145bb7a86e3658363c2c61474616e` | ## Development seed acceptance PR #942's `make seed` command was also exercised on the deployed system with the committed [Project-only fixture](fixtures/project-member-permissions-1001.yaml): ```sh make seed TARGET=dev-vasilis-m3 SSH=k3s-m3 \ SEED=docs/qualification/fixtures/project-member-permissions-1001.yaml ``` The first run created the Project and its two memberships: Alice as manager and Bob as ordinary member. The second run reported zero created and three present. The seed made no offerings or resource orders and did not write to GitLab. Source Control provisioned group 504 for canonical Project `a6057dfa-44b2-476c-bcce-a237f1873f69`. After real AAI sign-in, Bob retained Beto's `developer` role and received GitLab Maintainer access. He created a model repository through Themelio, cloned it and pushed commit `b80f08e3355ce0c28568726d19364ab886e5b4c9` to protected `main`. The remote ref matched the commit and branch protection still required Maintainer access. All 11 acceptance assertions passed. The cleanup attempt as Bob returned HTTP 404 because repository deletion requires Project administration. Alice then scheduled deletion successfully. The test token was revoked and both retained sessions logged out. The seeded Project and memberships remain available for repeated qualification. ## Verification | Lane | Result | | --- | --- | | Source Control unit suite | 460 passed, including existing-group upgrade, stale-authority ceiling and revocation after a delayed snapshot. | | `pnpm verify` | Passed, including 4,677 Metallo tests, Go race checks, builds, contract checks and architecture checks. Two platform-specific DX cases skipped. | | `pnpm test:integration` on `builder-m3` | Passed with rootful Docker as the normal user: real PostgreSQL/RabbitMQ/Valkey, 506 Metallo infrastructure tests, 19 LDAP checks, service images and 15 browser checks. | | `qualify_source_control_groups.py` | 44 checks passed against real GitLab. | | `qualify_source_control_handlers.py` | 23 checks passed, including lost-response recovery, failed removal retry, stale authority, Party disablement, binding retirement and closure. | | `qualify_source_control_layout.py` | 24 checks passed, including creation by both roles and upgrade of an existing Developer. | | `make seed` and seeded browser/Git journey | First run: three created. Repeat: zero created, three present. All 11 acceptance assertions passed as Bob. | | Existing-cluster deployment | `make reconcile TARGET=dev-vasilis-m3 SSH=k3s-m3 DEPLOY_RUNNER=builder-m3 BUILD_MODE=runner` succeeded. | The standalone GitLab probes run the production worker, reconciliation and handlers against real GitLab, with explicit in-memory owner/store fixtures. The deployed browser journey above uses the real owners and their persistence. Four separately gated authentication-navigation browser cases and three gated Go messaging/profile cases were skipped in the integration suite. Manual UAT, external federation/MFA, Minikube, fresh k3s installation, Slurm/HPC and the 101-member GitLab edge probe were not run. ## Corrections during qualification The first integration run used `builder-m3`'s default rootless Docker context. Its LDAP Unix socket mapped the host user to container root, failing the restricted SASL identity assertion. The complete suite then passed using the host's rootful socket, with temporary process-level socket group access. The older rename probe tried to create a repository directly in the Project group. It now uses the `models` subgroup, matching the enforced layout; the full probe passed afterwards. Two initial browser attempts interacted with the server-rendered form before its React handlers were ready, producing a timeout and an empty-payload HTTP 400. Waiting for the form's page loading to finish produced the complete 29-assertion pass. Two initial AUP waits also needed a fresh login during account preparation. These attempts are separate from the final acceptance result; this run does not qualify a fix for form hydration or onboarding. ## Cleanup and evidence The fixture prefix was `q817e661003-pr1001`. Repository cleanup used the canonical deletion API and reached `scheduled`; administrative cleanup then removed the two owned GitLab groups. Reconciliation recreated their empty Project namespaces, which are retained. Waldur marks the two Projects as removed (`is_removed=true`) and retains their records; the disposable Waldur accounts and temporary administrator were removed. Explicit Git tokens were revoked and the three retained sessions logged out. Tokens from interrupted fixture logins were also revoked. GitLab completed deletion of all three disposable accounts, with their absence verified by provider ID. Their Keycloak identities and temporary administration Jobs and Secrets were removed. The standalone GitLab probes each verified zero remaining owned groups/users. No Run, allocation or workload volume was created. Private driver, baseline, result and cleanup files are retained under `.local/qualification/pr1001/` in the qualification checkout. They contain fixture session material and are excluded from Git. This report contains no credentials.