feat: Users API, ownership transfer and removal of the legacy role string
This commit is contained in:
1 parent
094df63a7f
commit
80f7037d3a
124 files changed
+4644
-824
No files matched your search
@@ -25,13 +25,12 @@ type meBody struct {
|
||||
ID uint32 `json:"id"`
|
||||
Username string `json:"username"`
|
||||
IsVerified bool `json:"is_verified"`
|
||||
Role string `json:"role"`
|
||||
Author any `json:"author"`
|
||||
Access accessBody `json:"access"`
|
||||
}
|
||||
|
||||
func (e *apiKeyEnv) addUser(id uint32, legacy user.Role, roleID uint32, verified bool) {
|
||||
e.users.users[id] = user.User{ID: id, Username: "user" + string(rune('a'+id)), Email: "user" + string(rune('a'+id)) + "@example.com", IsVerified: verified, Role: legacy, RoleID: roleID}
|
||||
func (e *apiKeyEnv) addUser(id uint32, roleID uint32, verified bool) {
|
||||
e.users.users[id] = user.User{ID: id, Username: "user" + string(rune('a'+id)), Email: "user" + string(rune('a'+id)) + "@example.com", IsVerified: verified, RoleID: roleID}
|
||||
}
|
||||
|
||||
func (e *apiKeyEnv) me(t *testing.T, id uint32) (*httptest.ResponseRecorder, meBody) {
|
||||
@@ -57,12 +56,12 @@ func permissionKeys(grants []role.Grant) []string {
|
||||
func TestRolesPermissionsFoundationHTTP(t *testing.T) {
|
||||
t.Run("The owner's /me carries the Owner role and all 34 permissions", func(t *testing.T) {
|
||||
e := newAPIKeyEnv(t)
|
||||
e.addUser(7, user.RoleOwner, ownerRoleID, true)
|
||||
e.addUser(7, ownerRoleID, true)
|
||||
rec, body := e.me(t, 7)
|
||||
if rec.Code != http.StatusOK || rec.Header().Get("Cache-Control") != "no-store" {
|
||||
t.Fatalf("status %d cache %q", rec.Code, rec.Header().Get("Cache-Control"))
|
||||
}
|
||||
if body.ID != 7 || body.Role != "owner" || !body.IsVerified {
|
||||
if body.ID != 7 || !body.IsVerified {
|
||||
t.Errorf("existing fields changed: %+v", body)
|
||||
}
|
||||
if body.Access.Role.ID != ownerRoleID || body.Access.Role.Name != "Owner" || !body.Access.Role.BuiltIn || !body.Access.Role.Locked {
|
||||
@@ -84,7 +83,7 @@ func TestRolesPermissionsFoundationHTTP(t *testing.T) {
|
||||
|
||||
t.Run("An admin's /me omits the owner-only keys", func(t *testing.T) {
|
||||
e := newAPIKeyEnv(t)
|
||||
e.addUser(8, user.RoleAdmin, adminRoleID, true)
|
||||
e.addUser(8, adminRoleID, true)
|
||||
_, body := e.me(t, 8)
|
||||
if body.Access.Role.Name != "Admin" || !body.Access.Role.BuiltIn || body.Access.Role.Locked {
|
||||
t.Errorf("role %+v", body.Access.Role)
|
||||
@@ -105,7 +104,7 @@ func TestRolesPermissionsFoundationHTTP(t *testing.T) {
|
||||
|
||||
t.Run("A writer's /me lists only granted keys with their scope", func(t *testing.T) {
|
||||
e := newAPIKeyEnv(t)
|
||||
e.addUser(9, user.RoleAdmin, writerRoleID, true)
|
||||
e.addUser(9, writerRoleID, true)
|
||||
_, body := e.me(t, 9)
|
||||
want := []role.Grant{
|
||||
{Key: "content.read", Scope: role.ScopeAll}, {Key: "content.create", Scope: role.ScopeAll},
|
||||
@@ -124,7 +123,7 @@ func TestRolesPermissionsFoundationHTTP(t *testing.T) {
|
||||
custom, _ := e.roles.Create(role.Role{Name: "Contributor", Version: 1, Grants: []role.Grant{
|
||||
{Key: "content.update", Scope: role.ScopeOwn}, {Key: "content.read", Scope: role.ScopeAll},
|
||||
}})
|
||||
e.addUser(10, user.RoleAdmin, custom.ID, true)
|
||||
e.addUser(10, custom.ID, true)
|
||||
_, body := e.me(t, 10)
|
||||
want := []role.Grant{{Key: "content.read", Scope: role.ScopeAll}, {Key: "content.update", Scope: role.ScopeOwn}}
|
||||
if body.Access.Role.Name != "Contributor" || body.Access.Role.BuiltIn || !slices.Equal(body.Access.Permissions, want) {
|
||||
@@ -132,20 +131,16 @@ func TestRolesPermissionsFoundationHTTP(t *testing.T) {
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("A user with no role_id falls back to the built-in role named by the legacy role", func(t *testing.T) {
|
||||
for legacy, name := range map[user.Role]string{user.RoleOwner: "Owner", user.RoleAdmin: "Admin"} {
|
||||
e := newAPIKeyEnv(t)
|
||||
e.addUser(11, legacy, 0, true)
|
||||
_, body := e.me(t, 11)
|
||||
if body.Access.Role.Name != name {
|
||||
t.Errorf("%s: got %q", legacy, body.Access.Role.Name)
|
||||
}
|
||||
}
|
||||
t.Run("A user with role_id 0 is answered 500 on /me and never promoted", func(t *testing.T) {
|
||||
e := newAPIKeyEnv(t)
|
||||
e.addUser(11, 0, true)
|
||||
rec, _ := e.me(t, 11)
|
||||
assertAPIKeyMessage(t, rec, http.StatusInternalServerError, "Internal server error")
|
||||
})
|
||||
|
||||
t.Run("The permissions follow the role live", func(t *testing.T) {
|
||||
e := newAPIKeyEnv(t)
|
||||
e.addUser(9, user.RoleAdmin, writerRoleID, true)
|
||||
e.addUser(9, writerRoleID, true)
|
||||
writer := e.roles.byName("Writer")
|
||||
writer.Grants = append(writer.Grants, role.Grant{Key: "media.delete", Scope: role.ScopeOwn})
|
||||
_, _ = e.roles.Save(writer)
|
||||
@@ -157,7 +152,7 @@ func TestRolesPermissionsFoundationHTTP(t *testing.T) {
|
||||
|
||||
t.Run("An unverified user still gets /me, with access", func(t *testing.T) {
|
||||
e := newAPIKeyEnv(t)
|
||||
e.addUser(12, user.RoleAdmin, writerRoleID, false)
|
||||
e.addUser(12, writerRoleID, false)
|
||||
rec, body := e.me(t, 12)
|
||||
if rec.Code != http.StatusOK || body.IsVerified || body.Access.Role.Name != "Writer" {
|
||||
t.Errorf("status %d body %+v", rec.Code, body)
|
||||
@@ -166,7 +161,7 @@ func TestRolesPermissionsFoundationHTTP(t *testing.T) {
|
||||
|
||||
t.Run("A user whose role cannot be resolved gets a server error, not an empty access block", func(t *testing.T) {
|
||||
e := newAPIKeyEnv(t)
|
||||
e.addUser(13, "", 99, true)
|
||||
e.addUser(13, 99, true)
|
||||
rec, _ := e.me(t, 13)
|
||||
if rec.Code != http.StatusInternalServerError || rec.Body.String() != "Internal server error\n" {
|
||||
t.Errorf("status %d body %q", rec.Code, rec.Body.String())
|
||||
@@ -178,7 +173,7 @@ func TestRolesPermissionsFoundationHTTP(t *testing.T) {
|
||||
if rec := e.anonymous(t, http.MethodGet, "/v1/auth/me", ""); rec.Code != http.StatusUnauthorized {
|
||||
t.Errorf("no cookie: %d", rec.Code)
|
||||
}
|
||||
e.addUser(7, user.RoleOwner, ownerRoleID, true)
|
||||
e.addUser(7, ownerRoleID, true)
|
||||
stale := e.users.users[7]
|
||||
stale.TokenVersion = 1
|
||||
e.users.users[7] = stale
|
||||
@@ -210,7 +205,7 @@ func TestPermissionCatalogueHTTP(t *testing.T) {
|
||||
|
||||
t.Run("A signed-in verified user reads the catalogue", func(t *testing.T) {
|
||||
e := newAPIKeyEnv(t)
|
||||
e.addUser(9, user.RoleAdmin, writerRoleID, true)
|
||||
e.addUser(9, writerRoleID, true)
|
||||
rec := e.withCookie(t, http.MethodGet, "/v1/permission", "", 9)
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf("status %d: %s", rec.Code, rec.Body.String())
|
||||
@@ -251,8 +246,8 @@ func TestPermissionCatalogueHTTP(t *testing.T) {
|
||||
|
||||
t.Run("The catalogue is the same for every role", func(t *testing.T) {
|
||||
e := newAPIKeyEnv(t)
|
||||
e.addUser(9, user.RoleAdmin, writerRoleID, true)
|
||||
e.addUser(7, user.RoleOwner, ownerRoleID, true)
|
||||
e.addUser(9, writerRoleID, true)
|
||||
e.addUser(7, ownerRoleID, true)
|
||||
writer := e.withCookie(t, http.MethodGet, "/v1/permission", "", 9)
|
||||
owner := e.withCookie(t, http.MethodGet, "/v1/permission", "", 7)
|
||||
if writer.Body.String() != owner.Body.String() || writer.Body.Len() == 0 {
|
||||
@@ -277,7 +272,7 @@ func TestPermissionCatalogueHTTP(t *testing.T) {
|
||||
|
||||
t.Run("A stale token is refused", func(t *testing.T) {
|
||||
e := newAPIKeyEnv(t)
|
||||
e.addUser(7, user.RoleOwner, ownerRoleID, true)
|
||||
e.addUser(7, ownerRoleID, true)
|
||||
stored := e.users.users[7]
|
||||
stored.TokenVersion = 1
|
||||
e.users.users[7] = stored
|
||||
@@ -289,7 +284,7 @@ func TestPermissionCatalogueHTTP(t *testing.T) {
|
||||
|
||||
t.Run("An unverified user is refused", func(t *testing.T) {
|
||||
e := newAPIKeyEnv(t)
|
||||
e.addUser(12, user.RoleAdmin, writerRoleID, false)
|
||||
e.addUser(12, writerRoleID, false)
|
||||
rec := e.withCookie(t, http.MethodGet, "/v1/permission", "", 12)
|
||||
if rec.Code != http.StatusForbidden || rec.Body.String() != apperr.VerificationRequiredErrorMessage+"\n" {
|
||||
t.Errorf("status %d body %q", rec.Code, rec.Body.String())
|
||||
|
||||
Reference in new issue
Block a user