package operator import ( "bytes" "context" "database/sql" "errors" "net/url" "amadan.net/oficina/home/internal/home/directorydata" ) type VerificationInput struct { InputBase Consumer string `json:"consumer"` TeamSQID string `json:"team_sqid"` App string `json:"app"` Audience string `json:"audience"` Generation string `json:"generation"` RecordSequence int64 `json:"record_sequence"` RecordDigest string `json:"record_digest"` Epoch int64 `json:"expected_epoch"` Owner string `json:"owner_ref"` Issued int64 `json:"issued_at"` Expires int64 `json:"expires_at"` } type ActivationEvidence struct { Version int `json:"version"` Policy string `json:"policy"` Attempt string `json:"attempt_id"` Consumer string `json:"consumer"` TeamSQID string `json:"team_sqid"` App string `json:"app"` Audience string `json:"audience"` Generation string `json:"generation"` RecordSequence int64 `json:"record_sequence"` RecordDigest string `json:"record_digest"` Epoch int64 `json:"epoch"` Owner string `json:"owner_ref"` AppVersion string `json:"app_version"` ArtifactDigest string `json:"artifact_digest"` EnvironmentBinding string `json:"environment_binding"` ManifestDigest string `json:"credential_manifest_digest"` AliasHost string `json:"alias_host"` Started int64 `json:"started_at"` Completed int64 `json:"completed_at"` } type ActivationInput struct { InputBase Evidence ActivationEvidence `json:"evidence"` } type pendingBinding struct { teamID, epoch int64 label string sequence int64 generation string digest []byte } func (d *Directory) pending(ctx context.Context, c *sql.Conn, consumer, team, app, audience, gen string, sequence int64, recordDigest, owner string, epoch int64) (pendingBinding, error) { fail := func() (pendingBinding, error) { return pendingBinding{}, ErrRefused } expected, ok := canonical(recordDigest, 32) if !atom(consumer, 64) || !d.apps[app] || !Origin(audience) || !CanonicalToken(gen) || sequence <= 0 || !ok || !PersonRef(owner) || epoch <= 0 { return fail() } var b pendingBinding var storedTeam, storedApp, storedOrigin, state string var enabled int err := c.QueryRowContext(ctx, `SELECT t.id,t.sqid,t.label,t.roster_epoch,i.app,i.origin,i.state,c.enabled 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 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) if err != nil { if errors.Is(err, sql.ErrNoRows) { return fail() } return pendingBinding{}, ErrUnavailable } if enabled != 1 || state != "pending" || storedTeam != team || storedApp != app || storedOrigin != audience || b.epoch != epoch { return fail() } if err = generation(ctx, c, gen); err != nil { return pendingBinding{}, err } var receiptSequence int64 var receiptGeneration, receiptConsumer, kind string var receiptDigest []byte err = c.QueryRowContext(ctx, `SELECT p.sequence,p.generation,p.record_digest,r.sequence,r.generation,r.record_digest,r.consumer_id,r.operation 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) if err != nil { return pendingBinding{}, ErrUnavailable } 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" { return fail() } var found int 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 { if errors.Is(err, sql.ErrNoRows) { return fail() } return pendingBinding{}, ErrUnavailable } var currentTokens int 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(¤tTokens); err != nil { return pendingBinding{}, ErrUnavailable } if currentTokens != 1 { return fail() } if !d.policy.ValidateRoute(directoryRoute(b, team, app, audience, consumer)) { return fail() } return b, nil } func (d *Directory) verifyConsumer(ctx context.Context, c *sql.Conn, r Request) ([]byte, error) { var in VerificationInput if DecodeInput(r, &in) != nil || !in.matches(r) || r.authorization.Initial { return nil, ErrRefused } now := d.engine.now().Unix() if in.Issued <= 0 || in.Issued > now || in.Expires <= now || in.Expires <= in.Issued || in.Expires-in.Issued > 600 { return nil, ErrRefused } binding, e := d.pending(ctx, c, in.Consumer, in.TeamSQID, in.App, in.Audience, in.Generation, in.RecordSequence, in.RecordDigest, in.Owner, in.Epoch) if e != nil { return nil, e } var old, oldConsumer string var issued, expires int64 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) if e == nil { if oldConsumer != in.Consumer || issued <= 0 || issued > now || expires <= issued || expires-issued > 600 || expires > now { return nil, ErrRefused } if _, e = c.ExecContext(ctx, "DELETE FROM verification_active WHERE consumer_id=? AND operation_id=?", in.Consumer, old); e != nil { return nil, e } 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 { return nil, e } } else if !errors.Is(e, sql.ErrNoRows) { return nil, ErrUnavailable } hash, ok := canonical(r.Head(), 32) if !ok { return nil, ErrUnavailable } _, 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) if e != nil { return nil, e } if _, e = c.ExecContext(ctx, "INSERT INTO verification_active VALUES(?,?)", in.Consumer, r.authorization.OperationID); e != nil { return nil, e } 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 { return nil, e } result := resultFor(r) result.Attempt = r.authorization.OperationID result.Expires = in.Expires return encode(result) } func version(s string) bool { if len(s) != 40 { return false } for _, c := range s { if !(c >= '0' && c <= '9' || c >= 'a' && c <= 'f') { return false } } return true } func (d *Directory) activate(ctx context.Context, c *sql.Conn, r Request) ([]byte, error) { var in ActivationInput if DecodeInput(r, &in) != nil || !in.matches(r) || r.authorization.Initial { return nil, ErrRefused } p := in.Evidence now := d.engine.now().Unix() u, e := url.Parse(p.Audience) 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 { return nil, ErrRefused } binding, e := d.pending(ctx, c, p.Consumer, p.TeamSQID, p.App, p.Audience, p.Generation, p.RecordSequence, p.RecordDigest, p.Owner, p.Epoch) if e != nil { return nil, e } var consumer, app, origin, gen, owner string var teamID, sequence, epoch, issued, expires int64 var recordDigest []byte 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 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) if e != nil { if errors.Is(e, sql.ErrNoRows) { return nil, ErrRefused } return nil, ErrUnavailable } 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 { return nil, ErrRefused } if _, e = c.ExecContext(ctx, "UPDATE team_instances SET state='live' WHERE consumer_id=? AND state='pending'", p.Consumer); e != nil { return nil, e } if _, e = c.ExecContext(ctx, "DELETE FROM verification_active WHERE consumer_id=? AND operation_id=?", p.Consumer, p.Attempt); e != nil { return nil, e } 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 { return nil, e } result := resultFor(r) result.Attempt = p.Attempt result.Generation = p.Generation result.TeamID = binding.teamID result.TeamSQID = p.TeamSQID result.Epoch = p.Epoch return encode(result) } func directoryRoute(b pendingBinding, team, app, audience, consumer string) directorydata.Route { return directorydata.Route{TeamID: b.teamID, TeamSQID: team, Label: b.label, App: app, Origin: audience, ConsumerID: consumer} }