| 1 | package docs |
| 2 | |
| 3 | import ( |
| 4 | "bytes" |
| 5 | "context" |
| 6 | "encoding/base64" |
| 7 | "errors" |
| 8 | "strings" |
| 9 | "testing" |
| 10 | |
| 11 | "amadan.net/oficina/home/roster" |
| 12 | family "amadan.net/rastrillo/rastrillo/crypto" |
| 13 | "amadan.net/rastrillo/rastrillo/migrate" |
| 14 | "github.com/carlosframework/docs/internal/docs/principalstore" |
| 15 | "github.com/carlosframework/docs/internal/docs/teamstate" |
| 16 | "github.com/go-json-experiment/json" |
| 17 | "gorm.io/gorm" |
| 18 | ) |
| 19 | |
| 20 | func teamConfigFixture(t *testing.T) (*TeamConfig, *homeSettings) { |
| 21 | t.Helper() |
| 22 | op, e := family.Generate() |
| 23 | if e != nil { |
| 24 | t.Fatal(e) |
| 25 | } |
| 26 | key, e := family.Generate() |
| 27 | if e != nil { |
| 28 | t.Fatal(e) |
| 29 | } |
| 30 | b := roster.Bindings{Issuer: "https://id.example", Directory: "https://home.example", Consumer: "docs-A", App: "docs", Team: "TeamA", Audience: "https://docs.example"} |
| 31 | p := roster.Provisioning{Version: 1, Operation: "provision1", Sequence: 1, Bindings: b, Generation: base64.RawURLEncoding.EncodeToString(bytes.Repeat([]byte{1}, 32)), MinEpoch: 1, Keys: []roster.Key{{ID: "roster1", Public: base64.RawURLEncoding.EncodeToString(key.SignPub())}}} |
| 32 | raw, e := roster.SignProvisioning(op, "operator1", p) |
| 33 | if e != nil { |
| 34 | t.Fatal(e) |
| 35 | } |
| 36 | keys, _ := json.Marshal([]roster.Key{{ID: "operator1", Public: base64.RawURLEncoding.EncodeToString(op.SignPub())}}) |
| 37 | return &TeamConfig{TeamID: b.Team, DirectoryOrigin: b.Directory, ProvisioningBase64: base64.StdEncoding.EncodeToString(raw), OperatorKeysBase64: base64.StdEncoding.EncodeToString(keys), RosterToken: base64.RawURLEncoding.EncodeToString(bytes.Repeat([]byte{7}, 32))}, &homeSettings{issuer: b.Issuer, consumerID: b.Consumer, dek: bytes.Repeat([]byte{9}, 32)} |
| 38 | } |
| 39 | func TestTeamConfigPinsAndColdMarker(t *testing.T) { |
| 40 | cfg, home := teamConfigFixture(t) |
| 41 | settings, e := validatedTeamConfig(cfg, home, "https://docs.example") |
| 42 | if e != nil { |
| 43 | t.Fatal(e) |
| 44 | } |
| 45 | for _, change := range []func(*TeamConfig){func(c *TeamConfig) { c.TeamID = "Other" }, func(c *TeamConfig) { c.DirectoryOrigin = "https://other.example" }, func(c *TeamConfig) { c.RosterToken += "=" }, func(c *TeamConfig) { c.ProvisioningBase64 += "\n" }, func(c *TeamConfig) { c.OperatorKeysBase64 = "e30=" }} { |
| 46 | bad := *cfg |
| 47 | change(&bad) |
| 48 | if _, e := validatedTeamConfig(&bad, home, "https://docs.example"); e != errTeamConfig { |
| 49 | t.Fatal("bad team config") |
| 50 | } |
| 51 | } |
| 52 | if _, e := validatedTeamConfig(cfg, nil, "https://docs.example"); e != errTeamConfig { |
| 53 | t.Fatal("team without Home") |
| 54 | } |
| 55 | app := newSampleDocumentsInternalTestApp(t, "0") |
| 56 | if _, e := migrate.Apply(t.Context(), app.db, TeamBootSchema); e != nil { |
| 57 | t.Fatal(e) |
| 58 | } |
| 59 | rt, e := newTeamRuntime(t.Context(), settings, app.db.Writer(), home) |
| 60 | if e != nil { |
| 61 | t.Fatal(e) |
| 62 | } |
| 63 | if rt.cache == nil || rt.principals == nil { |
| 64 | t.Fatal("runtime missing") |
| 65 | } |
| 66 | if got, yes, e := teamstate.Required(t.Context(), app.db.Writer()); e != nil || !yes || got != settings.fingerprint { |
| 67 | t.Fatal("runtime did not commit marker") |
| 68 | } |
| 69 | var count int |
| 70 | _ = app.db.Writer().QueryRow("SELECT COUNT(*) FROM docs_home_identities").Scan(&count) |
| 71 | if count != 0 { |
| 72 | t.Fatal("runtime seeded identity") |
| 73 | } |
| 74 | } |
| 75 | func TestTeamIdentityAddressAbsenceNeverRedeemsEmptyGrant(t *testing.T) { |
| 76 | app := newSampleDocumentsInternalTestApp(t, "0") |
| 77 | ctx := context.Background() |
| 78 | s := app.Store() |
| 79 | local, e := s.EnsurePerson(ctx, "owner@example.com") |
| 80 | if e != nil { |
| 81 | t.Fatal(e) |
| 82 | } |
| 83 | doc, e := s.Create(ctx, local, "private") |
| 84 | if e != nil { |
| 85 | t.Fatal(e) |
| 86 | } |
| 87 | if _, e = migrate.Apply(ctx, app.db, TeamBootSchema); e != nil { |
| 88 | t.Fatal(e) |
| 89 | } |
| 90 | ps, e := principalstore.New(app.db.Writer(), principalstore.Config{Issuer: "https://id.example", Team: "TeamA", RequireMembership: func(context.Context, principalstore.MembershipRequest) error { return nil }}) |
| 91 | if e != nil { |
| 92 | t.Fatal(e) |
| 93 | } |
| 94 | subject := "pr_" + strings.Repeat("a", 26) |
| 95 | p, e := ps.EnsureHome(ctx, principalstore.HomeIdentity{Issuer: "https://id.example", Subject: subject}) |
| 96 | if e != nil { |
| 97 | t.Fatal(e) |
| 98 | } |
| 99 | rt, _, _ := testTeamRoster(t, s, app) |
| 100 | rt.principals = ps |
| 101 | s.team = rt |
| 102 | resolver := identityResolver{store: s, team: rt} |
| 103 | id, e := resolver.resolve(identityRequest("GET", subject, homeAssertionMethod)) |
| 104 | if e != nil || id.Actor.PersonID != p.ID || id.Actor.VerifiedAddress != nil || id.HomeSubject != subject { |
| 105 | t.Fatal("wrong Home principal", e) |
| 106 | } |
| 107 | ctx = context.WithValue(ctx, accessContextKey{}, rt.resolveAccess(ctx, id)) |
| 108 | if e = s.db.Create(&Grant{DocID: doc.ID, Subject: "", Role: RoleWriter, CreatedBy: local}).Error; e != nil { |
| 109 | t.Fatal(e) |
| 110 | } |
| 111 | if _, e = s.GetFor(ctx, id.Actor, doc.Sqid); !errors.Is(e, gorm.ErrRecordNotFound) { |
| 112 | t.Fatal("nil address redeemed malformed grant", e) |
| 113 | } |
| 114 | if s.CanWrite(ctx, id.Actor, doc.ID) { |
| 115 | t.Fatal("nil address redeemed malformed writer grant") |
| 116 | } |
| 117 | own, e := s.Create(ctx, p.ID, "Home owned") |
| 118 | if e != nil { |
| 119 | t.Fatal(e) |
| 120 | } |
| 121 | if e = s.Grant(ctx, p.ID, own.Sqid, "guest@example.com", RoleReader); e != nil { |
| 122 | t.Fatal("addressless owner could not grant", e) |
| 123 | } |
| 124 | var people int64 |
| 125 | s.db.Model(&Person{}).Count(&people) |
| 126 | if people != 1 { |
| 127 | t.Fatal("Home identity created legacy person") |
| 128 | } |
| 129 | if _, e = resolver.resolve(identityRequest("GET", "pr_"+strings.Repeat("b", 26), homeAssertionMethod)); e != errIdentityRefused { |
| 130 | t.Fatal("unknown identity allocated") |
| 131 | } |
| 132 | if _, e = resolver.resolve(identityRequest("GET", "unknown@example.com", "unsupported")); e != errIdentityRefused { |
| 133 | t.Fatal("unsupported local method accepted") |
| 134 | } |
| 135 | } |
| 136 | |