.gitbay/wiki/Architecture/05-Identity-and-Access.org
133 lines · 9725 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= | none | =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 | none | =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=).