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

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

147 lines · 11066 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; credential-minting and access-granting commands only within 15 minutes of sign-in | 12 h idle, 7 days absolute    | 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| Mail reply token   | HMAC-SHA256 (96 bits) over account, repository, thread, expiry, in the Reply-To local part | not stored (stateless); a =Message-ID= that posted is kept | one comment thread, as one account, only from that account's verified =From= | 30 days | with the account's access, checked when the reply is read; =notifications settings reply off= |
 26| LFS transfer token | HMAC-SHA256 over repo, SSH key, operation, expiry | not stored (stateless)       | one repository, upload or download         | 1 h                           | with its key: refused once the key is removed or expires, its account is disabled, or it loses the access the operation needs |
 27
 28Generation and hashing: =internal/store/sessions.go= (=NewToken=,
 29=HashToken=, =crypto/rand=). Cookie attributes: =HttpOnly=,
 30=SameSite=Lax=, =Secure= unless TLS is off, =MaxAge= 7 days
 31(the session itself also ends after 12 hours idle)
 32(=internal/httpd/accounts.go=). Token scope values are
 33constrained by a database =CHECK= as well as the command
 34(=internal/store/migrations/0004_api_tokens.up.sql=).
 35
 36What the scopes allow:
 37
 38| Scope          | Control commands                | git transport                     |
 39|----------------+---------------------------------+-----------------------------------|
 40| =full= key     | all the account may run         | read and write                    |
 41| =git= key      | none                            | read and write                    |
 42| =runner= key   | =runner *= only, on attached repositories | read (clone)            |
 43| =deploy:*= key | none                            | its repository only, =ro= or =rw= |
 44| =full= token   | all the account may run         | not over the API                  |
 45| =read= token   | commands marked =ReadOnly=      | not over the API                  |
 46
 47Sources: =internal/control/control.go=,
 48=internal/policy/access.go=.
 49
 50* Authorization decision
 51
 52Two layers decide every request.
 53
 541. *=Dispatch=* (=internal/control/control.go=), in order:
 55   scope gate, read-only gate, disabled account, =admin= noun, pending
 56   account, per-account write budget, stdin gating, handler, audit.
 572. *The handler*, which resolves the repository with =resolveRepo=
 58   (=internal/control/repo.go=) and a predicate from
 59   =internal/policy/access.go=:
 60
 61| Predicate  | True when                                                                |
 62|------------+--------------------------------------------------------------------------|
 63| =CanRead=  | owner; or the repository is public; or a grant of read, write or admin   |
 64| =CanWrite= | owner; or a grant of write or admin                                      |
 65| =CanAdmin= | owner; or a grant of admin                                               |
 66
 67Grants come from =repo_access= rows for users, organizations and teams.
 68Organization-owned repositories have no individual owner; members get
 69access only through grants.
 70
 71Not found versus denied: when the predicate fails, =resolveRepo=
 72checks =CanRead=. If the caller cannot read the repository the answer is
 73"not found", identical to a missing repository. Only a caller who can
 74read it gets "permission denied" for a write. git transport follows the
 75same rule (=internal/sshd/sshd.go=), and so does smart HTTP, which
 76serves public repositories only.
 77
 78Deploy keys bypass the grant model entirely: =runGit= checks only that
 79the key's scope names this repository and allows the operation
 80(=policy.DeployScopeAllows=, =internal/policy/access.go=).
 81
 82* Repository write protections
 83
 84Enforced in the pre-receive hook, so they apply to every credential
 85that reaches git transport (=internal/policy/access.go=,
 86=internal/hookd/hookd.go=):
 87
 88| Setting (=repo settings …=) | Effect                                                                  |
 89|---------------------------+-------------------------------------------------------------------------|
 90| =protect=                 | no deletion, no force push on the branch                                |
 91| =require-mr=              | no direct push to an existing protected branch; only the server's merge writes it |
 92| =protect-tag=             | no deletion or move of matching tags                                    |
 93| =require-signed=          | every incoming commit must verify (OpenPGP or SSHSIG) against a key registered to a verified email |
 94| (always)                  | =refs/merge-requests/*= is server-owned                                 |
 95
 96Merge gates, evaluated by =MergeGates= for =mr merge=, the web merge
 97button and the displayed status (=internal/control/mr.go=):
 98
 99| Gate                  | Satisfied when                                                              |
100|-----------------------+-----------------------------------------------------------------------------|
101| draft                 | the merge request is not a draft (always on)                                |
102| =require-checks=      | every status on the head is success, and some status exists if CI would have run |
103| =require-approvals N= | N fresh approvals from current writers other than the author, and no active request for changes |
104| =require-codeowners=  | every changed path matching a CODEOWNERS rule has an approval from a listed owner |
105| =require-resolved=    | no unresolved review threads                                                |
106
107* Session security on the web
108
109- CSRF: =SameSite=Lax= withholds the cookie on cross-site posts;
110  =checkOrigin= rejects a post whose =Origin= host differs from the
111  request host (=internal/httpd/accounts.go=).
112- Headers on every response: CSP =default-src 'self'; script-src
113  'none'; style-src 'self' 'unsafe-inline'; img-src * data:;
114  object-src 'none'; base-uri 'none'; form-action 'self';
115  frame-ancestors 'none'=, =X-Frame-Options: DENY=, =nosniff=,
116  =Referrer-Policy: no-referrer=, =Cross-Origin-Opener-Policy:
117  same-origin=, and HSTS for one year with subdomains when TLS is on
118  (=internal/httpd/routes.go=).
119- Destructive web actions (key, email and PGP removal, release, snippet,
120  team and label deletion, user disable and demote) require the target's
121  name typed into the form (=internal/httpd/confirm.go=).
122- Commands that create a credential (SSH, deploy and runner keys, API
123  tokens, email verification, login links, PGP keys, device tokens) or
124  grant access (repository and organization roles, teams, transfers,
125  repository visibility, admin promote and enable, webhooks, secrets,
126  mirrors) are refused
127  from a browser session that signed in more than 15 minutes ago
128  (=control.ReauthWindow=, =Command.NeedsRecentSignIn=). The sign-in
129  time is =web_sessions.created_at=, which idle renewal does not move;
130  a request with no sign-in time is refused. SSH, API tokens and host
131  commands are unaffected. The refusal is audited; the form shows it
132  with a sign-in link, and the login returns to the page through the
133  server-set =gitbay_next= cookie (=internal/httpd/flash.go=).
134
135* Rate limits
136
137| Limit                   | Default           | Keyed by              | Code                                  |
138|-------------------------+-------------------+-----------------------+---------------------------------------|
139| SSH auth failures       | 10 per minute     | client IP             | =internal/sshd/ratelimit.go=          |
140| API requests            | 120 per minute    | account, or IP if anonymous | =internal/httpd/apilimit.go=    |
141| API writes              | a tenth of the above | same               | =apilimit.go=                   |
142| Command writes, all surfaces | =limits.write_rate= (60 per minute) | account | =internal/control/control.go= |
143| Login links (web form)  | 5 per hour, plus per-IP | account and IP  | =internal/control/loginlink.go= |
144| Email verification mails| 5 per hour        | account               | =internal/control/register.go=    |
145
146=X-Forwarded-For= is honoured only from addresses listed in
147=http.trusted_proxies= (=internal/httpd/apilimit.go=).