Skip to main content

Push identity

Every decision GitProxy makes about a push starts with the same question: who is pushing? Whether the pusher is on the repository's contributor list, whether they may cancel the push later, and whether a reviewer is trying to approve their own change all depend on the answer.

The answer is taken from the credential that accompanied the push, never from the pushed objects. The author, committer and tagger lines inside a commit or tag are text the client wrote and can say anything. A push whose identity cannot be established from a credential is blocked.

Where the identity comes from​

How the push arrivesIdentity source
HTTPS with a token in the Basic-auth passwordThe upstream SCM is asked which account the token belongs to, and that account is looked up on GitProxy's user records.
SSHThe SSH key was already matched to a GitProxy user during authentication.
Dashboard sessionThe logged-in user.

For HTTPS pushes the Basic-auth username half is ignored: git sends whatever the user typed or their credential helper stored, and nothing verifies it. Only the token is used.

The step that does this is resolveUserFromToken. It runs first in both the branch and the tag chains.

Configuring SCM providers​

GitProxy needs to know how to ask each git host about a token. That is the scmProviders list in proxy.config.json:

"scmProviders": [
{ "name": "github", "type": "github", "host": "github.com" },
{ "name": "gitlab", "type": "gitlab", "host": "gitlab.com" },
{ "name": "codeberg", "type": "forgejo", "host": "codeberg.org" },
{ "name": "gitea", "type": "forgejo", "host": "gitea.com" }
]

Those four entries are the built-in defaults. Setting scmProviders in your own configuration replaces the list, so include the defaults you still want.

FieldMeaning
nameIdentifier for this provider. It is the key under which users link their account, so changing it unlinks everyone.
typeWhich API the host speaks: github, gitlab or forgejo. Gitea and Codeberg speak the Forgejo API.
hostHostname of the git remote, exactly as it appears in the repository URL. Matched case-insensitively, without a port.
apiUrlOptional base URL of the REST API. Derived from host and type when omitted; set it when the API is fronted from elsewhere or served on a non-default port.

How the API location is derived​

typehostEndpoint askedCredential sentField read
githubgithub.comhttps://api.github.com/userAuthorization: token …login
github<tenant>.ghe.comhttps://api.<tenant>.ghe.com/userAuthorization: token …login
githubany other (Enterprise Server)https://<host>/api/v3/userAuthorization: token …login
gitlabanyhttps://<host>/api/v4/userAuthorization: Bearer …username
forgejoanyhttps://<host>/api/v1/userAuthorization: token …login

The token needs whatever scope the host requires to read its own owner: read:user on GitHub and Forgejo-family hosts, read_user or api on GitLab. Fine-grained GitHub tokens can read the token owner without any extra permission.

Examples for self-hosted instances:

"scmProviders": [
{ "name": "github", "type": "github", "host": "github.com" },
{ "name": "ghes", "type": "github", "host": "github.corp.example.com" },
{ "name": "gitlab", "type": "gitlab", "host": "gitlab.corp.example.com" },
{ "name": "forge", "type": "forgejo", "host": "forge.corp.example.com" }
]

A push to a host with no matching provider is blocked with a message naming the host.

Well-known hosts are pinned to their type​

GitProxy refuses to start if github.com (or a *.ghe.com tenant), gitlab.com, codeberg.org or gitea.com is configured with any type other than its own. Configuring github.com as forgejo, for example, would send GitHub tokens to an API path that does not exist there and block every push to it, while a typo the other way would send Forgejo tokens to GitHub. Self-hosted hosts cannot be checked this way and are taken as configured, so review those entries with the same care as the rest of the authentication configuration. Duplicate names and duplicate hosts are also rejected.

Caching​

A token that resolved successfully is remembered for seven days, keyed by a SHA-512 hash of the provider name and the token. The cache stores which SCM account the token belongs to, not which GitProxy user; the account-to-user lookup runs on every push, so linking or unlinking an account takes effect immediately. Rejected tokens are not cached. The cache is in memory and empties on restart.

Linking accounts to users​

A user record carries a map of provider name to account handle:

{
"username": "jdoe",
"email": "jdoe@example.com",
"scmIdentities": { "github": "jdoe-gh", "gitlab": "jdoe" }
}

Handles are stored lower-cased and matched case-insensitively. A handle can be linked to one user per provider.

Link or unlink through the API while logged in:

# link your own account
curl -X POST http://localhost:8080/api/auth/scm-identity \
-H 'Content-Type: application/json' -b cookies \
-d '{"provider":"github","login":"jdoe-gh"}'

# an admin linking someone else's
curl -X POST http://localhost:8080/api/auth/scm-identity \
-H 'Content-Type: application/json' -b cookies \
-d '{"username":"jdoe","provider":"gitlab","login":"jdoe"}'

# unlink
curl -X POST http://localhost:8080/api/auth/scm-identity \
-H 'Content-Type: application/json' -b cookies \
-d '{"provider":"gitlab","login":null}'

Admins can also pass scmIdentities when creating a user through POST /api/auth/create-user.

What a pusher sees​

A push that cannot be attributed is rejected before any other check runs, with one of:

Push blocked: no credentials were presented, so the pusher cannot be identified.
Push blocked: github did not accept the credential presented with this push (invalid token, or missing the scope to read the token owner).
Push blocked: github account 'jdoe-gh' is not linked to a git-proxy user. Link it from your profile or ask an administrator.
Push blocked: no SCM provider is configured for host 'git.example.com', so the pusher cannot be identified. Add the host to 'scmProviders' in the proxy configuration.

Once attributed, checkUserPushPermission checks the resolved user against the repository's canPush list, and the dashboard's four-eyes rule compares the reviewer against the resolved user.

Upgrading an existing deployment​

This is a breaking change. Previously, when a token could not be resolved or the resolved account was not linked, GitProxy fell back to the last commit's committer line as the pusher. That fallback is gone.

What changes for an existing deployment:

  • Pushes to github.com keep working once each user's account is linked. The gitAccount field on existing users is carried over as their github identity by a migration on startup, so users who had set it correctly need to do nothing. Users whose gitAccount was blank, or the seed value none, have to link. A handle that more than one user had set, or that another user has already linked, identifies nobody: the migration links none of them and logs a warning, and an administrator has to decide who owns it.
  • Pushes held for approval before the upgrade cannot be approved. Their pusher was recorded from the pushed objects, so the four-eyes rule cannot be applied to them; the dashboard refuses with a message asking for a fresh push, and an approval already granted to such a push is not reused. Pushing again records the pusher from the credential and creates a reviewable record in its place.
  • Pushes to any other host are blocked until that host is listed under scmProviders.
  • Tag pushes now go through identity resolution too. Previously they were attributed to the tagger line.
  • POST /api/auth/gitAccount is replaced by POST /api/auth/scm-identity. POST /api/auth/create-user no longer requires gitAccount.
  • git-proxy-cli create-user no longer takes --gitAccount.
  • The repository canPush and canAuthorise lists are unchanged. They still hold GitProxy usernames.