diff options
author | 2024-07-31 09:26:09 -0700 | |
---|---|---|
committer | 2024-07-31 09:26:09 -0700 | |
commit | fd837776e2aaf30f4ea973d65c9dfe0979988371 (patch) | |
tree | c2853d4bab55e1eccd91cbf48df4dd6279bc72d7 /internal/typeutils | |
parent | [docs] Update system requirements, move things around a bit (#3157) (diff) | |
download | gotosocial-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.go | 101 | ||||
-rw-r--r-- | internal/typeutils/internaltofrontend_test.go | 222 |
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" }, |