| 1 | package docs |
| 2 | |
| 3 | import ( |
| 4 | "amadan.net/rastrillo/rastrillo/sessions" |
| 5 | "context" |
| 6 | "crypto/sha256" |
| 7 | "database/sql" |
| 8 | "encoding/base64" |
| 9 | "errors" |
| 10 | "net/http" |
| 11 | |
| 12 | "amadan.net/oficina/home/handoff/seal" |
| 13 | "amadan.net/oficina/home/roster" |
| 14 | "amadan.net/rastrillo/rastrillo/migrate" |
| 15 | "github.com/carlosframework/docs/internal/docs/principalstore" |
| 16 | "github.com/carlosframework/docs/internal/docs/teamstate" |
| 17 | "github.com/go-json-experiment/json" |
| 18 | ) |
| 19 | |
| 20 | // TeamConfig is explicit deployment configuration, never browser request input. |
| 21 | type TeamConfig struct { |
| 22 | transport http.RoundTripper |
| 23 | clock roster.Clock |
| 24 | TeamID string |
| 25 | DirectoryOrigin string |
| 26 | ProvisioningBase64 string |
| 27 | OperatorKeysBase64 string |
| 28 | RosterToken string |
| 29 | } |
| 30 | |
| 31 | var errTeamConfig = errors.New("docs: invalid team configuration") |
| 32 | var errTeamUnavailable = errors.New("docs: team authority unavailable") |
| 33 | |
| 34 | type teamSettings struct { |
| 35 | transport http.RoundTripper |
| 36 | clock roster.Clock |
| 37 | record roster.VerifiedRecord |
| 38 | token string |
| 39 | fingerprint string |
| 40 | } |
| 41 | |
| 42 | var TeamBootSchema = migrate.Merge(BootSchema, principalstore.Schema, teamstate.Schema) |
| 43 | |
| 44 | func validatedTeamConfig(cfg *TeamConfig, home *homeSettings, origin string) (*teamSettings, error) { |
| 45 | if cfg == nil { |
| 46 | return nil, nil |
| 47 | } |
| 48 | if home == nil || !roster.ValidChallenge(cfg.RosterToken) { |
| 49 | return nil, errTeamConfig |
| 50 | } |
| 51 | raw, ok := homeConfigBase64(cfg.ProvisioningBase64, roster.BodyLimit) |
| 52 | if !ok { |
| 53 | return nil, errTeamConfig |
| 54 | } |
| 55 | encoded, ok := homeConfigBase64(cfg.OperatorKeysBase64, 4096) |
| 56 | if !ok { |
| 57 | return nil, errTeamConfig |
| 58 | } |
| 59 | var keys []roster.Key |
| 60 | if json.Unmarshal(encoded, &keys, json.RejectUnknownMembers(true)) != nil { |
| 61 | return nil, errTeamConfig |
| 62 | } |
| 63 | expected := roster.Bindings{Issuer: home.issuer, Directory: cfg.DirectoryOrigin, Consumer: home.consumerID, App: "docs", Team: cfg.TeamID, Audience: origin} |
| 64 | record, e := roster.VerifyProvisioning(raw, keys, expected) |
| 65 | if e != nil { |
| 66 | return nil, errTeamConfig |
| 67 | } |
| 68 | b, e := json.Marshal(record.Bindings()) |
| 69 | if e != nil { |
| 70 | return nil, errTeamConfig |
| 71 | } |
| 72 | digest := sha256.Sum256(b) |
| 73 | return &teamSettings{transport: cfg.transport, clock: cfg.clock, record: record, token: cfg.RosterToken, fingerprint: base64.RawURLEncoding.EncodeToString(digest[:])}, nil |
| 74 | } |
| 75 | |
| 76 | type teamRuntime struct { |
| 77 | bridge *homeBridge |
| 78 | readSession func(context.Context, *http.Request) (sessions.Session, bool) |
| 79 | cache *roster.Cache |
| 80 | principals *principalstore.Store |
| 81 | bindings roster.Bindings |
| 82 | db *sql.DB |
| 83 | } |
| 84 | |
| 85 | func newTeamRuntime(ctx context.Context, settings *teamSettings, db *sql.DB, home *homeSettings) (*teamRuntime, error) { |
| 86 | if settings == nil || db == nil || home == nil { |
| 87 | return nil, errTeamConfig |
| 88 | } |
| 89 | b := settings.record.Bindings() |
| 90 | var foreign int |
| 91 | if e := db.QueryRowContext(ctx, "SELECT (SELECT COUNT(*) FROM docs_identity_links WHERE issuer!=?)+(SELECT COUNT(*) FROM docs_home_identities WHERE issuer!=?)", b.Issuer, b.Issuer).Scan(&foreign); e != nil { |
| 92 | return nil, errTeamUnavailable |
| 93 | } |
| 94 | if foreign != 0 { |
| 95 | return nil, errTeamConfig |
| 96 | } |
| 97 | if e := teamstate.Activate(ctx, db, settings.fingerprint); e != nil { |
| 98 | return nil, errTeamConfig |
| 99 | } |
| 100 | sealer, e := seal.New(home.dek, "oficina/docs") |
| 101 | if e != nil { |
| 102 | return nil, errTeamConfig |
| 103 | } |
| 104 | cache, e := roster.New(ctx, roster.Config{Record: settings.record, Token: settings.token, Transport: settings.transport, Clock: settings.clock, Durable: teamstate.Evidence{DB: db}, Sealer: sealer}) |
| 105 | if e != nil { |
| 106 | return nil, errTeamUnavailable |
| 107 | } |
| 108 | runtime := &teamRuntime{cache: cache, bindings: b, db: db} |
| 109 | principals, e := principalstore.New(db, principalstore.Config{Issuer: b.Issuer, Team: b.Team, RequireMembership: func(ctx context.Context, request principalstore.MembershipRequest) error { |
| 110 | d := cache.Check(ctx, request.Issuer, request.Team, request.Subject) |
| 111 | if d.State == roster.Member && d.Lease.Valid() { |
| 112 | return nil |
| 113 | } |
| 114 | if d.State == roster.NotMember { |
| 115 | return principalstore.ErrRefused |
| 116 | } |
| 117 | return principalstore.ErrUnavailable |
| 118 | }}) |
| 119 | if e != nil { |
| 120 | return nil, errTeamUnavailable |
| 121 | } |
| 122 | runtime.principals = principals |
| 123 | return runtime, nil |
| 124 | } |
| 125 | |