This is the model people most often get wrong: the iterion user who launches a bot is NOT who the bot acts as on the forge. Two completely separate planes are at play. This page documents both and how they meet (they barely do).
viewer | member | admin | owner in an
org/team) governs what you can do inside iterion — log in, launch runs,
manage settings/integrations/keys. GitHub team-gating (SSO grants) feeds
this plane: being in org/team gives you an iterion seat.So: “users in a GitHub team that has repo rights → do they inherit those rights to make PRs?” → No. Their GitHub membership only gated their login to the iterion org. The PR is opened with the connection’s token.
Per-team role, ordered viewer(1) < member(2) < admin(3) < owner(4)
(pkg/identity/types.go). Gates iterion operations:
launch runs (member+), manage integrations / keys / members / SSO
(canManageTeam = admin+, pkg/server/auth_routes.go).
It does not choose or change any forge credential.
GitHub SSO team-gating populates this plane only: a verified+enabled grant
(github_org, team_slug) → member lets matching users log in and join the
iterion org. The GitHub OAuth login token is used once to read the user’s
email + org/team memberships and is then discarded — never stored, never
reused for forge writes (pkg/auth/oidc/github.go,
pkg/auth/oidc_service.go).
A forge.Connection (pkg/forge/types.go) is a
team-scoped service credential. Its sealed token is opened
(AdminTokenFor, pkg/forge/connection_sealer.go)
and re-packaged once as a managed forge_token generic secret
(ensureManagedSecret, pkg/forge/orchestrator.go),
injected into every bot run at /run/iterion/secrets/forge_token. The bot’s
gh/glab/git calls authenticate with that token — i.e. the connection’s
identity. (The managed secret holds the same token value as the connection;
the re-seal only changes the encryption envelope, not the identity.)
| Connection kind | Token | Acts on the forge as | Repo rights = |
|---|---|---|---|
oauth_app |
OAuth user access token (refreshable) | the user who authorized the OAuth app | that user’s repo permissions |
github_app |
Installation token (minted ~1h, GitHub only) | the GitHub App (a bot identity) | the App’s manifest permissions (narrow, least-privilege) |
pat |
Personal access token (static) | the PAT owner (a person or a service account) | the PAT’s scopes |
The launching user’s iterion role is irrelevant to this: a member and an
admin launching the same bot both write via the same connection token.
Provision records the launching user only as ActorID / created_by for the
audit trail (pkg/forge/orchestrator.go) — never
for authentication.
If the GitHub connection is oauth_app authorized by an org-admin (e.g. the
super-admin’s own account), then every bot run that any team user launches
opens PRs / pushes as that org-admin, with that account’s full GitHub
powers — a shared, broad service credential. Convenient, but high blast radius:
a plain iterion member who can launch a bot effectively wields the connecting
admin’s GitHub rights on the repos that account can reach.
Being a GitHub org-admin yourself does not grant your bots anything extra beyond what the connection token can do; it only mattered because the org-admin is typically who authorized the connection.
Prefer narrowing the connection, not the user:
github_app) — the bot acts as the App with exactly the
permissions in its manifest (contents:write, pull_requests:write,
repository_hooks:write for the per-repo inbound webhook, metadata:read),
scoped to the repos the App is installed on. It deliberately does not
request administration (repo deletion/settings/teams/branch-protection) —
that is over-privileged, and per GitHub docs webhooks require
repository_hooks, not administration. The right answer for production: bots get only
what they need, and PRs are authored by a clearly-non-human bot identity.
Self-service (no platform App, no manual registration): Integrations →
“+ Register an OAuth app” → github → “Create a GitHub App” (iterion builds
the scoped App via manifest and captures its private key), then the “Install”
button on that app → install on the org/repos → a github_app connection. The
legacy platform App (ITERION_FORGE_GITHUB_APP_*) still works as a fallback.pat) with a minimal scope, instead of a
human org-admin’s oauth_app.The identity model is identical across providers — connection token = acting
identity — via the common forge.Admin abstraction (ADR-049,
docs/adr/049-forge-as-interchangeable-substrate.md).
| GitHub | GitLab | Forgejo/Gitea | |
|---|---|---|---|
oauth_app (acts as the authorizing user) |
✅ | ✅ | ✅ |
pat (acts as the token owner) |
✅ | ✅ | ✅ |
github_app (acts as a scoped App/bot) |
✅ | ✅ (best path on a self-hosted instance: a project/group access token used as a pat) |
— (no App concept; same — use a bot-account PAT) |
| auth header | Bearer |
Bearer |
token (Gitea scheme) |
So the only real difference is that the narrow “bot identity” path is a
first-class GitHub App on GitHub, whereas on GitLab/Forgejo you approximate
least-privilege with a scoped service-account / project token connected as a
pat. Clients: pkg/forge/gitlab/client.go,
pkg/forge/forgejo/client.go.
The forge action is authored by the connection identity, but who triggered
it is recorded: RepoIntegration.CreatedBy / the run’s ActorID and the
tenant audit log (pkg/audit). So “a PR opened by the connection
bot” is traceable to the iterion user (and run) that launched it, even though
the GitHub author is the connection.
When any of these ship, update this page.