package test import ( "RenewCMS/internal/domain/contenttype" "RenewCMS/internal/domain/entry" "RenewCMS/internal/domain/role" "RenewCMS/internal/domain/user" "RenewCMS/internal/infrastructure/useCases" "net/http" "net/http/httptest" "strconv" "testing" "time" ) const ( raOwner uint32 = 3 raAdmin uint32 = 5 raWriter uint32 = 9 raLegacyAdmin uint32 = 10 raUnverified uint32 = 12 raRoleManager uint32 = 31 raViewer uint32 = 32 raTypedManager uint32 = 33 raNarrowManager uint32 = 34 raEventsEditor uint32 = 35 raContributorOne uint32 = 41 raContentLead uint32 = 45 raEventsTypeID uint32 = 1 raVenuesTypeID uint32 = 2 raUnknownRoleID uint32 = 999 ) type rolesAPIRoleSpec struct { name string grants []string members []uint32 } var rolesAPICustomRoles = []rolesAPIRoleSpec{ {"Contributor", []string{"content.read=all", "content.create=all", "content.update=own"}, []uint32{41, 42, 43, 44}}, {"Intern", []string{"content.read=own"}, nil}, {"Content lead", []string{"content.read=all", "content.update=all", "content.publish=all", "content.delete=all"}, []uint32{raContentLead}}, {"Role manager", []string{"roles.view=all", "roles.manage=all", "content.read=all", "content.create=all", "content.update=own", "content.publish=own", "schema.view=all"}, []uint32{raRoleManager}}, {"Viewer", []string{"roles.view=all"}, []uint32{raViewer}}, {"Typed manager", []string{"roles.view=all", "roles.manage=all", "content.read=all", "content.update=all", "content.update:article=none"}, []uint32{raTypedManager}}, {"Narrow manager", []string{"roles.view=all", "roles.manage=all", "content.read:events=all"}, []uint32{raNarrowManager}}, {"Events editor", []string{"content.read=all", "content.update:events=own", "content.delete:events=none", "content.update:venues=all"}, []uint32{raEventsEditor}}, {"Typed target", []string{"content.read=all", "content.update=own", "content.update:article=none"}, nil}, } type rolesAPIWorld struct { *apiKeyEnv recorder *contentScopeRecorder types *mockContentTypeRepo entries *mockEntryRepo ids map[string]uint32 } func newRolesAPIWorld(t *testing.T) *rolesAPIWorld { t.Helper() recorder := &contentScopeRecorder{} env := newAPIKeyEnvRecording(t, recorder) world := &rolesAPIWorld{apiKeyEnv: env, recorder: recorder, types: newMockContentTypeRepo(), entries: newMockEntryRepo(), ids: map[string]uint32{}} env.users.users = map[uint32]user.User{} env.roles.users = env.users world.seedBuiltIns() world.seedRoles() world.seedContentTypes() world.wire() return world } func (w *rolesAPIWorld) seedBuiltIns() { w.ids["Owner"], w.ids["Admin"], w.ids["Writer"] = ownerRoleID, adminRoleID, writerRoleID w.addUser(raOwner, user.RoleOwner, ownerRoleID, true) w.addUser(raAdmin, user.RoleAdmin, adminRoleID, true) w.addUser(raLegacyAdmin, user.RoleAdmin, 0, true) w.addUser(raWriter, "", writerRoleID, true) w.addUser(raUnverified, user.RoleAdmin, adminRoleID, false) } func (w *rolesAPIWorld) seedRoles() { for _, spec := range rolesAPICustomRoles { created, _ := w.roles.Create(role.Role{Name: spec.name, Version: 1, Grants: grantRows(spec.grants...)}) w.ids[spec.name] = created.ID for _, member := range spec.members { w.addUser(member, "", created.ID, true) } } } func (w *rolesAPIWorld) seedContentTypes() { title := contenttype.Field{Key: "title", Label: "Title", Type: contenttype.TypeText, Required: true, Options: []string{}} w.types.types[raEventsTypeID] = contenttype.ContentType{ID: raEventsTypeID, Key: "events", Name: "Events", Fields: []contenttype.Field{title}} w.types.types[raVenuesTypeID] = contenttype.ContentType{ID: raVenuesTypeID, Key: "venues", Name: "Venues", Fields: []contenttype.Field{title}} w.types.nextID = 10 } func validContentTypeFields() []contenttype.Field { return []contenttype.Field{{Key: "title", Label: "Title", Type: contenttype.TypeText, Required: true, Options: []string{}}} } func entryOfType(typeID uint32) entry.Entry { return entry.Entry{ID: 1, ContentTypeID: typeID} } func (w *rolesAPIWorld) clock() time.Time { return w.now } func (w *rolesAPIWorld) wire() { c := w.container c.ListRolesUseCase = useCases.NewListRolesUseCase(w.roles, w.users, w.types) c.GetRoleUseCase = useCases.NewGetRoleUseCase(w.roles, w.users, w.types) c.CreateRoleUseCase = useCases.NewCreateRoleUseCase(w.roles, w.users, w.types, w.recorder) c.UpdateRoleUseCase = useCases.NewUpdateRoleUseCase(w.roles, w.users, w.types, w.recorder, w.clock) c.DeleteRoleUseCase = useCases.NewDeleteRoleUseCase(w.roles, w.users, w.types, w.recorder) c.ReassignRoleMembersUseCase = useCases.NewReassignRoleMembersUseCase(w.roles, w.users, w.types, w.recorder) c.CreateContentTypeUseCase = useCases.NewCreateContentTypeUseCase(w.types, w.roles, w.clock) c.ListContentTypesUseCase = useCases.NewListContentTypesUseCase(w.types) c.GetContentTypeUseCase = useCases.NewGetContentTypeUseCase(w.types) c.UpdateContentTypeUseCase = useCases.NewUpdateContentTypeUseCase(w.types, w.entries, w.clock) c.DeleteContentTypeUseCase = useCases.NewDeleteContentTypeUseCase(w.types, w.entries, w.roles) w.rebuildRouter() } func (w *rolesAPIWorld) role(name string) role.Role { found, err := w.roles.Get(w.ids[name]) if err != nil { panic("unknown role " + name) } return found } func (w *rolesAPIWorld) path(name string) string { return "/v1/role/" + strconv.FormatUint(uint64(w.ids[name]), 10) } func (w *rolesAPIWorld) caller(userID uint32) useCases.ContentCaller { account := w.users.users[userID] roleID := account.RoleID if roleID == 0 { roleID = w.ids[map[user.Role]string{user.RoleOwner: "Owner", user.RoleAdmin: "Admin"}[account.Role]] } return useCases.ContentCaller{UserID: userID, Grants: w.roles.roles[roleID].GrantSet()} } func (w *rolesAPIWorld) members(name string) []uint32 { found := []uint32{} for _, id := range w.users.sortedIds() { if w.users.users[id].RoleID == w.ids[name] { found = append(found, id) } } return found } func (w *rolesAPIWorld) do(t *testing.T, caller uint32, method string, path string, body string) *httptest.ResponseRecorder { t.Helper() return w.send(t, apiKeyRequest{method: method, path: path, body: body, cookieUser: caller}) } func (w *rolesAPIWorld) resetCounters() { w.roles.calls = map[string]int{} w.types.listCalls = 0 } func rolesAPIUpdateBody(name string, description string, version uint32, grants ...string) string { return `{"name": ` + strconv.Quote(name) + `, "description": ` + strconv.Quote(description) + `, "version": ` + strconv.FormatUint(uint64(version), 10) + `, "grants": ` + rolesAPIGrantsJSON(grants...) + `}` } func rolesAPIGrantsJSON(rows ...string) string { body := "[" for index, grant := range grantRows(rows...) { if index > 0 { body += ", " } body += `{"key": ` + strconv.Quote(grant.Key) + `, "scope": ` + strconv.Quote(string(grant.Scope)) + `}` } return body + "]" } func rolesAPIContributorRows(extra ...string) []string { return append([]string{"content.read=all", "content.create=all", "content.update=own"}, extra...) } func assertRefusal(t *testing.T, rec *httptest.ResponseRecorder, status int, code string) { t.Helper() if rec.Code != status || rec.Body.String() != code+"\n" { t.Fatalf("expected %d %q, got %d %q", status, code, rec.Code, rec.Body.String()) } if rec.Header().Get("X-Missing-Permission") != "" || rec.Header().Get("X-Escalation-Key") != "" { t.Fatalf("unexpected permission header on %q", code) } } func assertEscalation(t *testing.T, rec *httptest.ResponseRecorder, key string) { t.Helper() if rec.Code != http.StatusForbidden || rec.Body.String() != "escalation\n" { t.Fatalf("expected 403 escalation, got %d %q", rec.Code, rec.Body.String()) } if header := rec.Header().Get("X-Escalation-Key"); key != "" && header != key { t.Fatalf("expected X-Escalation-Key %q, got %q", key, header) } }