.gitbay/wiki/Architecture/05-Identity-and-Access.org

1ed9fb9399b21da8e6cf45be8389792aac82d5bc
gitbay/.gitbay/wiki/Architecture/05-Identity-and-Access.org rendered · source · history · blame · raw

133 lines · 9733 bytes

  1#+title: Identity and access
  2
  3[[file:diagrams/05-authorization.svg]]
  4
  5* Accounts
  6
  7| Property     | Values / behaviour                                                                  | Code |
  8|--------------+-------------------------------------------------------------------------------------+------|
  9| Registration | =closed= (host bootstrap only), =invite= (single-use code bound to an email), =open= (email required) | =internal/control/register.go= |
 10| Pending      | open registrations start pending until email is verified; a pending account may run only =email verify=, =email add=, =whoami=, =help=, and has no git access | =internal/control/control.go=, =internal/sshd/sshd.go= |
 11| Disabled     | refused in =Dispatch=, in SSH exec and at login-link redemption                     | =control.go= |
 12| Admin        | =users.is_admin=; set only by =admin user create --admin= or promotion by an admin; gates the whole =admin= noun | =control.go= |
 13
 14* Credentials
 15
 16| Credential         | Format and generation                           | Stored as                        | Scope                                      | Expiry                        | Revocation                          |
 17|--------------------+-------------------------------------------------+----------------------------------+--------------------------------------------+-------------------------------+-------------------------------------|
 18| SSH user key       | user's public key                               | fingerprint and public blob      | =full=, =git=, or =runner=                 | optional =--ttl=, refused at auth | =keys remove= (own keys); closes its connections |
 19| Deploy key         | public key                                      | same table, scope =deploy:<repo>:ro/rw= | one repository, read or read-write  | optional =--ttl=, refused at auth | =repo deploy-key remove= (repo admin); closes its connections |
 20| API token          | =gb_= + 32 random bytes hex                     | SHA-256 hash                     | =read= (default) or =full=; with an expiry, no credential-minting command | optional =--ttl=              | =token revoke [--created]=          |
 21| Web session        | 32 random bytes hex, cookie =gitbay_session=    | SHA-256 hash                     | full account                               | 7 days, no sliding renewal    | logout, =web sessions revoke=       |
 22| Login link         | 32 random bytes hex in a URL                    | SHA-256 hash, single use         | creates a web session                      | 15 min (mail), 5 min (SSH)    | consumed on use                     |
 23| Email verification | 32 random bytes hex                             | SHA-256 hash, single use         | verifies one address for one account       | 24 h                          | consumed on use                     |
 24| Invite             | random code                                     | SHA-256 hash, single use         | one registration for one email             | as issued                     | consumed on use                     |
 25| LFS transfer token | HMAC-SHA256 over repo, operation, expiry        | not stored (stateless)           | one repository, upload or download         | 1 h                           | expiry only                         |
 26
 27Generation and hashing: =internal/store/sessions.go= (=NewToken=,
 28=HashToken=, =crypto/rand=). Cookie attributes: =HttpOnly=,
 29=SameSite=Lax=, =Secure= unless TLS is off, =MaxAge= 7 days
 30(=internal/httpd/accounts.go=). Token scope values are
 31constrained by a database =CHECK= as well as the command
 32(=internal/store/migrations/0004_api_tokens.up.sql=).
 33
 34What the scopes allow:
 35
 36| Scope          | Control commands                | git transport                     |
 37|----------------+---------------------------------+-----------------------------------|
 38| =full= key     | all the account may run         | read and write                    |
 39| =git= key      | none                            | read and write                    |
 40| =runner= key   | =runner *= only, on attached repositories | read (clone)            |
 41| =deploy:*= key | none                            | its repository only, =ro= or =rw= |
 42| =full= token   | all the account may run         | not over the API                  |
 43| =read= token   | commands marked =ReadOnly=      | not over the API                  |
 44
 45Sources: =internal/control/control.go=,
 46=internal/policy/access.go=.
 47
 48* Authorization decision
 49
 50Two layers decide every request.
 51
 521. *=Dispatch=* (=internal/control/control.go=), in order:
 53   scope gate, read-only gate, disabled account, =admin= noun, pending
 54   account, per-account write budget, stdin gating, handler, audit.
 552. *The handler*, which resolves the repository with =resolveRepo=
 56   (=internal/control/repo.go=) and a predicate from
 57   =internal/policy/access.go=:
 58
 59| Predicate  | True when                                                                |
 60|------------+--------------------------------------------------------------------------|
 61| =CanRead=  | owner; or the repository is public; or a grant of read, write or admin   |
 62| =CanWrite= | owner; or a grant of write or admin                                      |
 63| =CanAdmin= | owner; or a grant of admin                                               |
 64
 65Grants come from =repo_access= rows for users, organizations and teams.
 66Organization-owned repositories have no individual owner; members get
 67access only through grants.
 68
 69Not found versus denied: when the predicate fails, =resolveRepo=
 70checks =CanRead=. If the caller cannot read the repository the answer is
 71"not found", identical to a missing repository. Only a caller who can
 72read it gets "permission denied" for a write. git transport follows the
 73same rule (=internal/sshd/sshd.go=), and so does smart HTTP, which
 74serves public repositories only.
 75
 76Deploy keys bypass the grant model entirely: =runGit= checks only that
 77the key's scope names this repository and allows the operation
 78(=policy.DeployScopeAllows=, =internal/policy/access.go=).
 79
 80* Repository write protections
 81
 82Enforced in the pre-receive hook, so they apply to every credential
 83that reaches git transport (=internal/policy/access.go=,
 84=internal/hookd/hookd.go=):
 85
 86| Setting (=repo settings …=) | Effect                                                                  |
 87|---------------------------+-------------------------------------------------------------------------|
 88| =protect=                 | no deletion, no force push on the branch                                |
 89| =require-mr=              | no direct push to an existing protected branch; only the server's merge writes it |
 90| =protect-tag=             | no deletion or move of matching tags                                    |
 91| =require-signed=          | every incoming commit must verify (OpenPGP or SSHSIG) against a key registered to a verified email |
 92| (always)                  | =refs/merge-requests/*= is server-owned                                 |
 93
 94Merge gates, evaluated by =MergeGates= for =mr merge=, the web merge
 95button and the displayed status (=internal/control/mr.go=):
 96
 97| Gate                  | Satisfied when                                                              |
 98|-----------------------+-----------------------------------------------------------------------------|
 99| draft                 | the merge request is not a draft (always on)                                |
100| =require-checks=      | every status on the head is success, and some status exists if CI would have run |
101| =require-approvals N= | N fresh approvals from current writers other than the author, and no active request for changes |
102| =require-codeowners=  | every changed path matching a CODEOWNERS rule has an approval from a listed owner |
103| =require-resolved=    | no unresolved review threads                                                |
104
105* Session security on the web
106
107- CSRF: =SameSite=Lax= withholds the cookie on cross-site posts;
108  =checkOrigin= rejects a post whose =Origin= host differs from the
109  request host (=internal/httpd/accounts.go=).
110- Headers on every response: CSP =default-src 'self'; script-src
111  'none'; style-src 'self' 'unsafe-inline'; img-src * data:;
112  object-src 'none'; base-uri 'none'; form-action 'self';
113  frame-ancestors 'none'=, =X-Frame-Options: DENY=, =nosniff=,
114  =Referrer-Policy: no-referrer=, =Cross-Origin-Opener-Policy:
115  same-origin=, and HSTS for one year with subdomains when TLS is on
116  (=internal/httpd/routes.go=).
117- Destructive web actions (key, email and PGP removal, release, snippet,
118  team and label deletion, user disable and demote) require the target's
119  name typed into the form (=internal/httpd/confirm.go=).
120
121* Rate limits
122
123| Limit                   | Default           | Keyed by              | Code                                  |
124|-------------------------+-------------------+-----------------------+---------------------------------------|
125| SSH auth failures       | 10 per minute     | client IP             | =internal/sshd/ratelimit.go=          |
126| API requests            | 120 per minute    | account, or IP if anonymous | =internal/httpd/apilimit.go=    |
127| API writes              | a tenth of the above | same               | =apilimit.go=                   |
128| Command writes, all surfaces | =limits.write_rate= (60 per minute) | account | =internal/control/control.go= |
129| Login links (web form)  | 5 per hour, plus per-IP | account and IP  | =internal/control/loginlink.go= |
130| Email verification mails| 5 per hour        | account               | =internal/control/register.go=    |
131
132=X-Forwarded-For= is honoured only from addresses listed in
133=http.trusted_proxies= (=internal/httpd/apilimit.go=).