oficina / home Public

Clone
git clone https://amadan.net/oficina/home

Plain git — no account needed to clone.

Download

Download this file

1package operator
2
3import (
4 "bytes"
5 "context"
6 "database/sql"
7 "errors"
8 "net/url"
9
10 "amadan.net/oficina/home/internal/home/directorydata"
11)
12
13type VerificationInput struct {
14 InputBase
15 Consumer string `json:"consumer"`
16 TeamSQID string `json:"team_sqid"`
17 App string `json:"app"`
18 Audience string `json:"audience"`
19 Generation string `json:"generation"`
20 RecordSequence int64 `json:"record_sequence"`
21 RecordDigest string `json:"record_digest"`
22 Epoch int64 `json:"expected_epoch"`
23 Owner string `json:"owner_ref"`
24 Issued int64 `json:"issued_at"`
25 Expires int64 `json:"expires_at"`
26}
27type ActivationEvidence struct {
28 Version int `json:"version"`
29 Policy string `json:"policy"`
30 Attempt string `json:"attempt_id"`
31 Consumer string `json:"consumer"`
32 TeamSQID string `json:"team_sqid"`
33 App string `json:"app"`
34 Audience string `json:"audience"`
35 Generation string `json:"generation"`
36 RecordSequence int64 `json:"record_sequence"`
37 RecordDigest string `json:"record_digest"`
38 Epoch int64 `json:"epoch"`
39 Owner string `json:"owner_ref"`
40 AppVersion string `json:"app_version"`
41 ArtifactDigest string `json:"artifact_digest"`
42 EnvironmentBinding string `json:"environment_binding"`
43 ManifestDigest string `json:"credential_manifest_digest"`
44 AliasHost string `json:"alias_host"`
45 Started int64 `json:"started_at"`
46 Completed int64 `json:"completed_at"`
47}
48type ActivationInput struct {
49 InputBase
50 Evidence ActivationEvidence `json:"evidence"`
51}
52
53type pendingBinding struct {
54 teamID, epoch int64
55 label string
56 sequence int64
57 generation string
58 digest []byte
59}
60
61func (d *Directory) pending(ctx context.Context, c *sql.Conn, consumer, team, app, audience, gen string, sequence int64, recordDigest, owner string, epoch int64) (pendingBinding, error) {
62 fail := func() (pendingBinding, error) { return pendingBinding{}, ErrRefused }
63 expected, ok := canonical(recordDigest, 32)
64 if !atom(consumer, 64) || !d.apps[app] || !Origin(audience) || !CanonicalToken(gen) || sequence <= 0 || !ok || !PersonRef(owner) || epoch <= 0 {
65 return fail()
66 }
67 var b pendingBinding
68 var storedTeam, storedApp, storedOrigin, state string
69 var enabled int
70 err := c.QueryRowContext(ctx, `SELECT t.id,t.sqid,t.label,t.roster_epoch,i.app,i.origin,i.state,c.enabled
71 FROM roster_consumers c JOIN team_instances i ON i.consumer_id=c.consumer_id AND i.team_id=c.team_id AND i.app=c.app AND i.origin=c.origin
72 JOIN teams t ON t.id=c.team_id WHERE c.consumer_id=?`, consumer).Scan(&b.teamID, &storedTeam, &b.label, &b.epoch, &storedApp, &storedOrigin, &state, &enabled)
73 if err != nil {
74 if errors.Is(err, sql.ErrNoRows) {
75 return fail()
76 }
77 return pendingBinding{}, ErrUnavailable
78 }
79 if enabled != 1 || state != "pending" || storedTeam != team || storedApp != app || storedOrigin != audience || b.epoch != epoch {
80 return fail()
81 }
82 if err = generation(ctx, c, gen); err != nil {
83 return pendingBinding{}, err
84 }
85 var receiptSequence int64
86 var receiptGeneration, receiptConsumer, kind string
87 var receiptDigest []byte
88 err = c.QueryRowContext(ctx, `SELECT p.sequence,p.generation,p.record_digest,r.sequence,r.generation,r.record_digest,r.consumer_id,r.operation
89 FROM directory_operator_consumer_records p JOIN roster_operator_receipts r ON r.operation_id=p.operation_id WHERE p.consumer_id=?`, consumer).Scan(&b.sequence, &b.generation, &b.digest, &receiptSequence, &receiptGeneration, &receiptDigest, &receiptConsumer, &kind)
90 if err != nil {
91 return pendingBinding{}, ErrUnavailable
92 }
93 if b.sequence != sequence || b.generation != gen || !bytes.Equal(b.digest, expected) || receiptSequence != b.sequence || receiptGeneration != b.generation || !bytes.Equal(receiptDigest, b.digest) || receiptConsumer != consumer || kind != "provision" {
94 return fail()
95 }
96 var found int
97 if err = c.QueryRowContext(ctx, "SELECT 1 FROM memberships WHERE team_id=? AND person_ref=? AND role='owner'", b.teamID, owner).Scan(&found); err != nil {
98 if errors.Is(err, sql.ErrNoRows) {
99 return fail()
100 }
101 return pendingBinding{}, ErrUnavailable
102 }
103 var currentTokens int
104 if err = c.QueryRowContext(ctx, "SELECT count(*) FROM roster_tokens WHERE consumer_id=? AND slot='current' AND expires_at IS NULL AND issued_at<=?", consumer, d.engine.now().Unix()).Scan(&currentTokens); err != nil {
105 return pendingBinding{}, ErrUnavailable
106 }
107 if currentTokens != 1 {
108 return fail()
109 }
110 if !d.policy.ValidateRoute(directoryRoute(b, team, app, audience, consumer)) {
111 return fail()
112 }
113 return b, nil
114}
115func (d *Directory) verifyConsumer(ctx context.Context, c *sql.Conn, r Request) ([]byte, error) {
116 var in VerificationInput
117 if DecodeInput(r, &in) != nil || !in.matches(r) || r.authorization.Initial {
118 return nil, ErrRefused
119 }
120 now := d.engine.now().Unix()
121 if in.Issued <= 0 || in.Issued > now || in.Expires <= now || in.Expires <= in.Issued || in.Expires-in.Issued > 600 {
122 return nil, ErrRefused
123 }
124 binding, e := d.pending(ctx, c, in.Consumer, in.TeamSQID, in.App, in.Audience, in.Generation, in.RecordSequence, in.RecordDigest, in.Owner, in.Epoch)
125 if e != nil {
126 return nil, e
127 }
128 var old, oldConsumer string
129 var issued, expires int64
130 e = c.QueryRowContext(ctx, "SELECT a.operation_id,a.consumer_id,a.issued_at,a.expires_at FROM verification_active v JOIN verification_attempts a ON a.operation_id=v.operation_id WHERE v.consumer_id=?", in.Consumer).Scan(&old, &oldConsumer, &issued, &expires)
131 if e == nil {
132 if oldConsumer != in.Consumer || issued <= 0 || issued > now || expires <= issued || expires-issued > 600 || expires > now {
133 return nil, ErrRefused
134 }
135 if _, e = c.ExecContext(ctx, "DELETE FROM verification_active WHERE consumer_id=? AND operation_id=?", in.Consumer, old); e != nil {
136 return nil, e
137 }
138 if _, e = c.ExecContext(ctx, "INSERT INTO verification_outcomes(attempt_id,operator_operation_id,kind,completed_at) VALUES(?,?,'expired',?)", old, r.authorization.OperationID, now); e != nil {
139 return nil, e
140 }
141 } else if !errors.Is(e, sql.ErrNoRows) {
142 return nil, ErrUnavailable
143 }
144 hash, ok := canonical(r.Head(), 32)
145 if !ok {
146 return nil, ErrUnavailable
147 }
148 _, e = c.ExecContext(ctx, "INSERT INTO verification_attempts VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?)", r.authorization.OperationID, in.Consumer, binding.teamID, in.App, in.Audience, in.Generation, in.RecordSequence, binding.digest, in.Epoch, in.Owner, in.Issued, in.Expires, hash)
149 if e != nil {
150 return nil, e
151 }
152 if _, e = c.ExecContext(ctx, "INSERT INTO verification_active VALUES(?,?)", in.Consumer, r.authorization.OperationID); e != nil {
153 return nil, e
154 }
155 if _, e = c.ExecContext(ctx, "INSERT INTO verification_outcomes(attempt_id,operator_operation_id,kind,completed_at) VALUES(?,?,'opened',?)", r.authorization.OperationID, r.authorization.OperationID, now); e != nil {
156 return nil, e
157 }
158 result := resultFor(r)
159 result.Attempt = r.authorization.OperationID
160 result.Expires = in.Expires
161 return encode(result)
162}
163func version(s string) bool {
164 if len(s) != 40 {
165 return false
166 }
167 for _, c := range s {
168 if !(c >= '0' && c <= '9' || c >= 'a' && c <= 'f') {
169 return false
170 }
171 }
172 return true
173}
174func (d *Directory) activate(ctx context.Context, c *sql.Conn, r Request) ([]byte, error) {
175 var in ActivationInput
176 if DecodeInput(r, &in) != nil || !in.matches(r) || r.authorization.Initial {
177 return nil, ErrRefused
178 }
179 p := in.Evidence
180 now := d.engine.now().Unix()
181 u, e := url.Parse(p.Audience)
182 if e != nil || p.Version != 1 || p.Policy != "oficina/team-activation/v1" || !CanonicalToken(p.Attempt) || !version(p.AppVersion) || !CanonicalToken(p.ArtifactDigest) || !CanonicalToken(p.EnvironmentBinding) || !CanonicalToken(p.ManifestDigest) || p.AliasHost != u.Host || p.Started <= 0 || p.Completed < p.Started || p.Completed > now || now-p.Completed >= 300 {
183 return nil, ErrRefused
184 }
185 binding, e := d.pending(ctx, c, p.Consumer, p.TeamSQID, p.App, p.Audience, p.Generation, p.RecordSequence, p.RecordDigest, p.Owner, p.Epoch)
186 if e != nil {
187 return nil, e
188 }
189 var consumer, app, origin, gen, owner string
190 var teamID, sequence, epoch, issued, expires int64
191 var recordDigest []byte
192 e = c.QueryRowContext(ctx, `SELECT a.consumer_id,a.team_id,a.app,a.origin,a.generation,a.record_sequence,a.record_digest,a.expected_epoch,a.owner_ref,a.issued_at,a.expires_at
193 FROM verification_active v JOIN verification_attempts a ON a.operation_id=v.operation_id WHERE v.consumer_id=? AND a.operation_id=?`, p.Consumer, p.Attempt).Scan(&consumer, &teamID, &app, &origin, &gen, &sequence, &recordDigest, &epoch, &owner, &issued, &expires)
194 if e != nil {
195 if errors.Is(e, sql.ErrNoRows) {
196 return nil, ErrRefused
197 }
198 return nil, ErrUnavailable
199 }
200 if consumer != p.Consumer || teamID != binding.teamID || app != p.App || origin != p.Audience || gen != p.Generation || sequence != p.RecordSequence || !bytes.Equal(recordDigest, binding.digest) || epoch != p.Epoch || owner != p.Owner || issued <= 0 || issued > now || expires <= now || expires <= issued || expires-issued > 600 || p.Started < issued || p.Completed >= expires {
201 return nil, ErrRefused
202 }
203 if _, e = c.ExecContext(ctx, "UPDATE team_instances SET state='live' WHERE consumer_id=? AND state='pending'", p.Consumer); e != nil {
204 return nil, e
205 }
206 if _, e = c.ExecContext(ctx, "DELETE FROM verification_active WHERE consumer_id=? AND operation_id=?", p.Consumer, p.Attempt); e != nil {
207 return nil, e
208 }
209 if _, e = c.ExecContext(ctx, "INSERT INTO verification_outcomes(attempt_id,operator_operation_id,kind,completed_at) VALUES(?,?,'activated',?)", p.Attempt, r.authorization.OperationID, now); e != nil {
210 return nil, e
211 }
212 result := resultFor(r)
213 result.Attempt = p.Attempt
214 result.Generation = p.Generation
215 result.TeamID = binding.teamID
216 result.TeamSQID = p.TeamSQID
217 result.Epoch = p.Epoch
218 return encode(result)
219}
220
221func directoryRoute(b pendingBinding, team, app, audience, consumer string) directorydata.Route {
222 return directorydata.Route{TeamID: b.teamID, TeamSQID: team, Label: b.label, App: app, Origin: audience, ConsumerID: consumer}
223}
224