summaryrefslogtreecommitdiff
path: root/internal/typeutils
diff options
context:
space:
mode:
authorLibravatar Vyr Cossont <VyrCossont@users.noreply.github.com>2024-07-31 09:26:09 -0700
committerLibravatar GitHub <noreply@github.com>2024-07-31 09:26:09 -0700
commitfd837776e2aaf30f4ea973d65c9dfe0979988371 (patch)
treec2853d4bab55e1eccd91cbf48df4dd6279bc72d7 /internal/typeutils
parent[docs] Update system requirements, move things around a bit (#3157) (diff)
downloadgotosocial-fd837776e2aaf30f4ea973d65c9dfe0979988371.tar.xz
[feature] Implement Mastodon-compatible roles (#3136)
* Implement Mastodon-compatible roles - `Account.role` should only be available through verify_credentials for checking current user's permissions - `Account.role` now carries a Mastodon-compatible permissions bitmap and a marker for whether it should be shown to the public - `Account.roles` added for *public* display roles (undocumented but stable since Mastodon 4.1) - Web template now uses only public display roles (no user-visible change here, we already special-cased the `user` role) * Handle verify_credentials case for default role * Update JSON exact-match tests * Address review comments * Add blocks bit to admin permissions bitmap
Diffstat (limited to 'internal/typeutils')
-rw-r--r--internal/typeutils/internaltofrontend.go101
-rw-r--r--internal/typeutils/internaltofrontend_test.go222
2 files changed, 212 insertions, 111 deletions
diff --git a/internal/typeutils/internaltofrontend.go b/internal/typeutils/internaltofrontend.go
index 70230cd89..b85d24eee 100644
--- a/internal/typeutils/internaltofrontend.go
+++ b/internal/typeutils/internaltofrontend.go
@@ -100,13 +100,13 @@ func (c *Converter) UserToAPIUser(ctx context.Context, u *gtsmodel.User) *apimod
return user
}
-// AppToAPIAppSensitive takes a db model application as a param, and returns a populated apitype application, or an error
+// AccountToAPIAccountSensitive takes a db model application as a param, and returns a populated apitype application, or an error
// if something goes wrong. The returned application should be ready to serialize on an API level, and may have sensitive fields
// (such as client id and client secret), so serve it only to an authorized user who should have permission to see it.
func (c *Converter) AccountToAPIAccountSensitive(ctx context.Context, a *gtsmodel.Account) (*apimodel.Account, error) {
// We can build this sensitive account model
// by first getting the public account, and
- // then adding the Source object to it.
+ // then adding the Source object and role permissions bitmap to it.
apiAccount, err := c.AccountToAPIAccountPublic(ctx, a)
if err != nil {
return nil, err
@@ -122,6 +122,13 @@ func (c *Converter) AccountToAPIAccountSensitive(ctx context.Context, a *gtsmode
}
}
+ // Populate the account's role permissions bitmap and highlightedness from its public role.
+ if len(apiAccount.Roles) > 0 {
+ apiAccount.Role = c.APIAccountDisplayRoleToAPIAccountRoleSensitive(&apiAccount.Roles[0])
+ } else {
+ apiAccount.Role = c.APIAccountDisplayRoleToAPIAccountRoleSensitive(nil)
+ }
+
statusContentType := string(apimodel.StatusContentTypeDefault)
if a.Settings.StatusContentType != "" {
statusContentType = a.Settings.StatusContentType
@@ -299,7 +306,7 @@ func (c *Converter) accountToAPIAccountPublic(ctx context.Context, a *gtsmodel.A
var (
acct string
- role *apimodel.AccountRole
+ roles []apimodel.AccountDisplayRole
enableRSS bool
theme string
customCSS string
@@ -324,14 +331,8 @@ func (c *Converter) accountToAPIAccountPublic(ctx context.Context, a *gtsmodel.A
if err != nil {
return nil, gtserror.Newf("error getting user from database for account id %s: %w", a.ID, err)
}
-
- switch {
- case *user.Admin:
- role = &apimodel.AccountRole{Name: apimodel.AccountRoleAdmin}
- case *user.Moderator:
- role = &apimodel.AccountRole{Name: apimodel.AccountRoleModerator}
- default:
- role = &apimodel.AccountRole{Name: apimodel.AccountRoleUser}
+ if role := c.UserToAPIAccountDisplayRole(user); role != nil {
+ roles = append(roles, *role)
}
enableRSS = *a.Settings.EnableRSS
@@ -380,7 +381,7 @@ func (c *Converter) accountToAPIAccountPublic(ctx context.Context, a *gtsmodel.A
CustomCSS: customCSS,
EnableRSS: enableRSS,
HideCollections: hideCollections,
- Role: role,
+ Roles: roles,
}
// Bodge default avatar + header in,
@@ -391,6 +392,56 @@ func (c *Converter) accountToAPIAccountPublic(ctx context.Context, a *gtsmodel.A
return accountFrontend, nil
}
+// UserToAPIAccountDisplayRole returns the API representation of a user's display role.
+// This will accept a nil user but does not always return a value:
+// the default "user" role is considered uninteresting and not returned.
+func (c *Converter) UserToAPIAccountDisplayRole(user *gtsmodel.User) *apimodel.AccountDisplayRole {
+ switch {
+ case user == nil:
+ return nil
+ case *user.Admin:
+ return &apimodel.AccountDisplayRole{
+ ID: string(apimodel.AccountRoleAdmin),
+ Name: apimodel.AccountRoleAdmin,
+ }
+ case *user.Moderator:
+ return &apimodel.AccountDisplayRole{
+ ID: string(apimodel.AccountRoleModerator),
+ Name: apimodel.AccountRoleModerator,
+ }
+ default:
+ return nil
+ }
+}
+
+// APIAccountDisplayRoleToAPIAccountRoleSensitive returns the API representation of a user's role,
+// with permission bitmap. This will accept a nil display role and always returns a value.
+func (c *Converter) APIAccountDisplayRoleToAPIAccountRoleSensitive(display *apimodel.AccountDisplayRole) *apimodel.AccountRole {
+ // Default to user role.
+ role := &apimodel.AccountRole{
+ AccountDisplayRole: apimodel.AccountDisplayRole{
+ ID: string(apimodel.AccountRoleUser),
+ Name: apimodel.AccountRoleUser,
+ },
+ Permissions: apimodel.AccountRolePermissionsNone,
+ Highlighted: false,
+ }
+
+ // If there's a display role, use that instead.
+ if display != nil {
+ role.AccountDisplayRole = *display
+ role.Highlighted = true
+ switch display.Name {
+ case apimodel.AccountRoleAdmin:
+ role.Permissions = apimodel.AccountRolePermissionsForAdminRole
+ case apimodel.AccountRoleModerator:
+ role.Permissions = apimodel.AccountRolePermissionsForModeratorRole
+ }
+ }
+
+ return role
+}
+
func (c *Converter) fieldsToAPIFields(f []*gtsmodel.Field) []apimodel.Field {
fields := make([]apimodel.Field, len(f))
@@ -416,8 +467,8 @@ func (c *Converter) fieldsToAPIFields(f []*gtsmodel.Field) []apimodel.Field {
// when someone wants to view an account they've blocked.
func (c *Converter) AccountToAPIAccountBlocked(ctx context.Context, a *gtsmodel.Account) (*apimodel.Account, error) {
var (
- acct string
- role *apimodel.AccountRole
+ acct string
+ roles []apimodel.AccountDisplayRole
)
if a.IsRemote() {
@@ -438,14 +489,8 @@ func (c *Converter) AccountToAPIAccountBlocked(ctx context.Context, a *gtsmodel.
if err != nil {
return nil, gtserror.Newf("error getting user from database for account id %s: %w", a.ID, err)
}
-
- switch {
- case *user.Admin:
- role = &apimodel.AccountRole{Name: apimodel.AccountRoleAdmin}
- case *user.Moderator:
- role = &apimodel.AccountRole{Name: apimodel.AccountRoleModerator}
- default:
- role = &apimodel.AccountRole{Name: apimodel.AccountRoleUser}
+ if role := c.UserToAPIAccountDisplayRole(user); role != nil {
+ roles = append(roles, *role)
}
}
@@ -464,7 +509,7 @@ func (c *Converter) AccountToAPIAccountBlocked(ctx context.Context, a *gtsmodel.
// Empty array (not nillable).
Fields: make([]apimodel.Field, 0),
Suspended: !a.SuspendedAt.IsZero(),
- Role: role,
+ Roles: roles,
}
// Don't show the account's actual
@@ -487,7 +532,7 @@ func (c *Converter) AccountToAdminAPIAccount(ctx context.Context, a *gtsmodel.Ac
inviteRequest *string
approved bool
disabled bool
- role = apimodel.AccountRole{Name: apimodel.AccountRoleUser} // assume user by default
+ role = *c.APIAccountDisplayRoleToAPIAccountRoleSensitive(nil)
createdByApplicationID string
)
@@ -527,11 +572,9 @@ func (c *Converter) AccountToAdminAPIAccount(ctx context.Context, a *gtsmodel.Ac
inviteRequest = &user.Reason
}
- if *user.Admin {
- role.Name = apimodel.AccountRoleAdmin
- } else if *user.Moderator {
- role.Name = apimodel.AccountRoleModerator
- }
+ role = *c.APIAccountDisplayRoleToAPIAccountRoleSensitive(
+ c.UserToAPIAccountDisplayRole(user),
+ )
confirmed = !user.ConfirmedAt.IsZero()
approved = *user.Approved
diff --git a/internal/typeutils/internaltofrontend_test.go b/internal/typeutils/internaltofrontend_test.go
index 46f6c2455..307b5f163 100644
--- a/internal/typeutils/internaltofrontend_test.go
+++ b/internal/typeutils/internaltofrontend_test.go
@@ -68,10 +68,7 @@ func (suite *InternalToFrontendTestSuite) TestAccountToFrontend() {
"last_status_at": "2024-01-10T09:24:00.000Z",
"emojis": [],
"fields": [],
- "enable_rss": true,
- "role": {
- "name": "user"
- }
+ "enable_rss": true
}`, string(b))
}
@@ -135,7 +132,11 @@ func (suite *InternalToFrontendTestSuite) TestAccountToFrontendAliasedAndMoved()
},
"enable_rss": true,
"role": {
- "name": "user"
+ "id": "user",
+ "name": "user",
+ "color": "",
+ "permissions": "0",
+ "highlighted": false
},
"moved": {
"id": "01F8MH5NBDF2MV7CTC4Q5128HF",
@@ -169,10 +170,7 @@ func (suite *InternalToFrontendTestSuite) TestAccountToFrontendAliasedAndMoved()
"verified_at": null
}
],
- "hide_collections": true,
- "role": {
- "name": "user"
- }
+ "hide_collections": true
}
}`, string(b))
}
@@ -222,10 +220,7 @@ func (suite *InternalToFrontendTestSuite) TestAccountToFrontendWithEmojiStruct()
}
],
"fields": [],
- "enable_rss": true,
- "role": {
- "name": "user"
- }
+ "enable_rss": true
}`, string(b))
}
@@ -272,10 +267,7 @@ func (suite *InternalToFrontendTestSuite) TestAccountToFrontendWithEmojiIDs() {
}
],
"fields": [],
- "enable_rss": true,
- "role": {
- "name": "user"
- }
+ "enable_rss": true
}`, string(b))
}
@@ -321,7 +313,11 @@ func (suite *InternalToFrontendTestSuite) TestAccountToFrontendSensitive() {
},
"enable_rss": true,
"role": {
- "name": "user"
+ "id": "user",
+ "name": "user",
+ "color": "",
+ "permissions": "0",
+ "highlighted": false
}
}`, string(b))
}
@@ -494,9 +490,13 @@ func (suite *InternalToFrontendTestSuite) TestStatusToFrontend() {
"emojis": [],
"fields": [],
"enable_rss": true,
- "role": {
- "name": "admin"
- }
+ "roles": [
+ {
+ "id": "admin",
+ "name": "admin",
+ "color": ""
+ }
+ ]
},
"media_attachments": [
{
@@ -667,9 +667,13 @@ func (suite *InternalToFrontendTestSuite) TestWarnFilteredStatusToFrontend() {
"emojis": [],
"fields": [],
"enable_rss": true,
- "role": {
- "name": "admin"
- }
+ "roles": [
+ {
+ "id": "admin",
+ "name": "admin",
+ "color": ""
+ }
+ ]
},
"media_attachments": [
{
@@ -849,10 +853,7 @@ func (suite *InternalToFrontendTestSuite) TestWarnFilteredBoostToFrontend() {
"last_status_at": "2024-01-10T09:24:00.000Z",
"emojis": [],
"fields": [],
- "enable_rss": true,
- "role": {
- "name": "user"
- }
+ "enable_rss": true
},
"media_attachments": [
{
@@ -980,9 +981,13 @@ func (suite *InternalToFrontendTestSuite) TestWarnFilteredBoostToFrontend() {
"emojis": [],
"fields": [],
"enable_rss": true,
- "role": {
- "name": "admin"
- }
+ "roles": [
+ {
+ "id": "admin",
+ "name": "admin",
+ "color": ""
+ }
+ ]
},
"media_attachments": [],
"mentions": [],
@@ -1518,9 +1523,13 @@ func (suite *InternalToFrontendTestSuite) TestStatusToFrontendUnknownLanguage()
"emojis": [],
"fields": [],
"enable_rss": true,
- "role": {
- "name": "admin"
- }
+ "roles": [
+ {
+ "id": "admin",
+ "name": "admin",
+ "color": ""
+ }
+ ]
},
"media_attachments": [
{
@@ -1657,10 +1666,7 @@ func (suite *InternalToFrontendTestSuite) TestStatusToFrontendPartialInteraction
"last_status_at": "2024-01-10T09:24:00.000Z",
"emojis": [],
"fields": [],
- "enable_rss": true,
- "role": {
- "name": "user"
- }
+ "enable_rss": true
},
"media_attachments": [],
"mentions": [],
@@ -1853,9 +1859,13 @@ func (suite *InternalToFrontendTestSuite) TestInstanceV1ToFrontend() {
"emojis": [],
"fields": [],
"enable_rss": true,
- "role": {
- "name": "admin"
- }
+ "roles": [
+ {
+ "id": "admin",
+ "name": "admin",
+ "color": ""
+ }
+ ]
},
"max_toot_chars": 5000,
"rules": [],
@@ -1989,9 +1999,13 @@ func (suite *InternalToFrontendTestSuite) TestInstanceV2ToFrontend() {
"emojis": [],
"fields": [],
"enable_rss": true,
- "role": {
- "name": "admin"
- }
+ "roles": [
+ {
+ "id": "admin",
+ "name": "admin",
+ "color": ""
+ }
+ ]
}
},
"rules": [],
@@ -2158,10 +2172,7 @@ func (suite *InternalToFrontendTestSuite) TestReportToFrontend2() {
"verified_at": null
}
],
- "hide_collections": true,
- "role": {
- "name": "user"
- }
+ "hide_collections": true
}
}`, string(b))
}
@@ -2194,7 +2205,11 @@ func (suite *InternalToFrontendTestSuite) TestAdminReportToFrontend1() {
"locale": "",
"invite_request": null,
"role": {
- "name": "user"
+ "id": "user",
+ "name": "user",
+ "color": "",
+ "permissions": "0",
+ "highlighted": false
},
"confirmed": false,
"approved": false,
@@ -2235,7 +2250,11 @@ func (suite *InternalToFrontendTestSuite) TestAdminReportToFrontend1() {
"locale": "en",
"invite_request": null,
"role": {
- "name": "user"
+ "id": "user",
+ "name": "user",
+ "color": "",
+ "permissions": "0",
+ "highlighted": false
},
"confirmed": true,
"approved": true,
@@ -2274,10 +2293,7 @@ func (suite *InternalToFrontendTestSuite) TestAdminReportToFrontend1() {
"verified_at": null
}
],
- "hide_collections": true,
- "role": {
- "name": "user"
- }
+ "hide_collections": true
},
"created_by_application_id": "01F8MGY43H3N2C8EWPR2FPYEXG"
},
@@ -2292,7 +2308,11 @@ func (suite *InternalToFrontendTestSuite) TestAdminReportToFrontend1() {
"locale": "en",
"invite_request": null,
"role": {
- "name": "admin"
+ "id": "admin",
+ "name": "admin",
+ "color": "",
+ "permissions": "546033",
+ "highlighted": true
},
"confirmed": true,
"approved": true,
@@ -2321,9 +2341,13 @@ func (suite *InternalToFrontendTestSuite) TestAdminReportToFrontend1() {
"emojis": [],
"fields": [],
"enable_rss": true,
- "role": {
- "name": "admin"
- }
+ "roles": [
+ {
+ "id": "admin",
+ "name": "admin",
+ "color": ""
+ }
+ ]
},
"created_by_application_id": "01F8MGXQRHYF5QPMTMXP78QC2F"
},
@@ -2338,7 +2362,11 @@ func (suite *InternalToFrontendTestSuite) TestAdminReportToFrontend1() {
"locale": "en",
"invite_request": null,
"role": {
- "name": "admin"
+ "id": "admin",
+ "name": "admin",
+ "color": "",
+ "permissions": "546033",
+ "highlighted": true
},
"confirmed": true,
"approved": true,
@@ -2367,9 +2395,13 @@ func (suite *InternalToFrontendTestSuite) TestAdminReportToFrontend1() {
"emojis": [],
"fields": [],
"enable_rss": true,
- "role": {
- "name": "admin"
- }
+ "roles": [
+ {
+ "id": "admin",
+ "name": "admin",
+ "color": ""
+ }
+ ]
},
"created_by_application_id": "01F8MGXQRHYF5QPMTMXP78QC2F"
},
@@ -2407,7 +2439,11 @@ func (suite *InternalToFrontendTestSuite) TestAdminReportToFrontend2() {
"locale": "en",
"invite_request": null,
"role": {
- "name": "user"
+ "id": "user",
+ "name": "user",
+ "color": "",
+ "permissions": "0",
+ "highlighted": false
},
"confirmed": true,
"approved": true,
@@ -2446,10 +2482,7 @@ func (suite *InternalToFrontendTestSuite) TestAdminReportToFrontend2() {
"verified_at": null
}
],
- "hide_collections": true,
- "role": {
- "name": "user"
- }
+ "hide_collections": true
},
"created_by_application_id": "01F8MGY43H3N2C8EWPR2FPYEXG"
},
@@ -2464,7 +2497,11 @@ func (suite *InternalToFrontendTestSuite) TestAdminReportToFrontend2() {
"locale": "",
"invite_request": null,
"role": {
- "name": "user"
+ "id": "user",
+ "name": "user",
+ "color": "",
+ "permissions": "0",
+ "highlighted": false
},
"confirmed": false,
"approved": false,
@@ -2665,7 +2702,11 @@ func (suite *InternalToFrontendTestSuite) TestAdminReportToFrontendSuspendedLoca
"locale": "",
"invite_request": null,
"role": {
- "name": "user"
+ "id": "user",
+ "name": "user",
+ "color": "",
+ "permissions": "0",
+ "highlighted": false
},
"confirmed": false,
"approved": false,
@@ -2706,7 +2747,11 @@ func (suite *InternalToFrontendTestSuite) TestAdminReportToFrontendSuspendedLoca
"locale": "",
"invite_request": null,
"role": {
- "name": "user"
+ "id": "user",
+ "name": "user",
+ "color": "",
+ "permissions": "0",
+ "highlighted": false
},
"confirmed": true,
"approved": true,
@@ -2735,10 +2780,7 @@ func (suite *InternalToFrontendTestSuite) TestAdminReportToFrontendSuspendedLoca
"emojis": [],
"fields": [],
"suspended": true,
- "hide_collections": true,
- "role": {
- "name": "user"
- }
+ "hide_collections": true
}
},
"assigned_account": {
@@ -2752,7 +2794,11 @@ func (suite *InternalToFrontendTestSuite) TestAdminReportToFrontendSuspendedLoca
"locale": "en",
"invite_request": null,
"role": {
- "name": "admin"
+ "id": "admin",
+ "name": "admin",
+ "color": "",
+ "permissions": "546033",
+ "highlighted": true
},
"confirmed": true,
"approved": true,
@@ -2781,9 +2827,13 @@ func (suite *InternalToFrontendTestSuite) TestAdminReportToFrontendSuspendedLoca
"emojis": [],
"fields": [],
"enable_rss": true,
- "role": {
- "name": "admin"
- }
+ "roles": [
+ {
+ "id": "admin",
+ "name": "admin",
+ "color": ""
+ }
+ ]
},
"created_by_application_id": "01F8MGXQRHYF5QPMTMXP78QC2F"
},
@@ -2798,7 +2848,11 @@ func (suite *InternalToFrontendTestSuite) TestAdminReportToFrontendSuspendedLoca
"locale": "en",
"invite_request": null,
"role": {
- "name": "admin"
+ "id": "admin",
+ "name": "admin",
+ "color": "",
+ "permissions": "546033",
+ "highlighted": true
},
"confirmed": true,
"approved": true,
@@ -2827,9 +2881,13 @@ func (suite *InternalToFrontendTestSuite) TestAdminReportToFrontendSuspendedLoca
"emojis": [],
"fields": [],
"enable_rss": true,
- "role": {
- "name": "admin"
- }
+ "roles": [
+ {
+ "id": "admin",
+ "name": "admin",
+ "color": ""
+ }
+ ]
},
"created_by_application_id": "01F8MGXQRHYF5QPMTMXP78QC2F"
},