From 6013a71ba45eb2a23e58b1764805ece12193a4f2 Mon Sep 17 00:00:00 2001 From: tobi Date: Sun, 2 Mar 2025 16:46:44 +0100 Subject: [PATCH] [feature] Refactor tokens, allow multiple app redirect_uris --- cmd/gotosocial/action/server/server.go | 2 +- docs/api/swagger.yaml | 20 +- internal/api/activitypub/users/user_test.go | 2 - internal/api/auth/auth_test.go | 2 - internal/api/auth/token_test.go | 101 +++++++-- internal/api/client/accounts/account_test.go | 2 - internal/api/client/admin/admin_test.go | 2 - .../api/client/bookmarks/bookmarks_test.go | 2 - internal/api/client/exports/exports_test.go | 2 - .../api/client/favourites/favourites_test.go | 2 - internal/api/client/filters/v1/filter_test.go | 2 - internal/api/client/filters/v2/filter_test.go | 2 - .../client/followedtags/followedtags_test.go | 2 - .../followrequests/followrequest_test.go | 2 - internal/api/client/import/import_test.go | 2 - internal/api/client/instance/instance_test.go | 2 - internal/api/client/lists/lists_test.go | 2 - internal/api/client/media/mediacreate_test.go | 4 +- internal/api/client/media/mediaupdate_test.go | 4 +- internal/api/client/mutes/mutes_test.go | 2 - .../notifications/notifications_test.go | 2 - internal/api/client/polls/polls_test.go | 2 - internal/api/client/push/push_test.go | 2 - internal/api/client/reports/reports_test.go | 2 - internal/api/client/search/search_test.go | 2 - internal/api/client/statuses/status_test.go | 2 - .../api/client/streaming/streaming_test.go | 2 - internal/api/client/tags/tags_test.go | 2 - internal/api/client/user/user_test.go | 2 - internal/api/fileserver/fileserver_test.go | 4 +- internal/api/model/application.go | 14 +- internal/api/util/auth.go | 54 +++++ internal/api/util/scopes.go | 26 ++- internal/api/util/scopes_test.go | 10 + .../api/wellknown/webfinger/webfinger_test.go | 4 +- .../wellknown/webfinger/webfingerget_test.go | 2 +- internal/cache/cache.go | 2 - internal/cache/db.go | 29 --- internal/cache/invalidate.go | 8 +- internal/cache/size.go | 31 +-- internal/db/application.go | 12 +- internal/db/bundb/admin.go | 21 +- internal/db/bundb/application.go | 50 ++--- internal/db/bundb/application_test.go | 7 - internal/db/bundb/bundb_test.go | 2 - ...0250224105654_token_app_client_refactor.go | 200 ++++++++++++++++++ .../application.go} | 13 +- .../token.go | 42 ++++ .../federatingdb/federatingdb_test.go | 2 - internal/filter/visibility/filter_test.go | 2 - internal/gtsmodel/application.go | 45 ++-- internal/gtsmodel/client.go | 30 --- internal/gtsmodel/token.go | 35 ++- internal/oauth/clientstore.go | 42 ++-- internal/oauth/clientstore_test.go | 76 ++----- internal/oauth/server.go | 141 +++++++++--- internal/oauth/tokenstore.go | 160 ++++++++++---- internal/processing/account/account_test.go | 2 - internal/processing/account/delete.go | 5 - internal/processing/admin/admin_test.go | 4 +- internal/processing/app.go | 60 +++--- .../conversations/conversations_test.go | 2 - internal/processing/media/media_test.go | 2 - internal/processing/processor_test.go | 4 +- internal/processing/status/status_test.go | 2 - internal/processing/stream/stream_test.go | 2 +- internal/processing/user/user_test.go | 4 +- internal/processing/workers/workers_test.go | 2 - internal/text/formatter_test.go | 2 - internal/transport/transport_test.go | 2 - internal/typeutils/internaltofrontend.go | 4 +- internal/webpush/realsender_test.go | 3 +- testrig/db.go | 7 - testrig/oauthserver.go | 12 +- testrig/processor.go | 2 +- testrig/testmodels.go | 47 +--- testrig/teststructs.go | 2 +- 77 files changed, 860 insertions(+), 553 deletions(-) create mode 100644 internal/db/bundb/migrations/20250224105654_token_app_client_refactor.go rename internal/{oauth/oauth_test.go => db/bundb/migrations/20250224105654_token_app_client_refactor/application.go} (62%) create mode 100644 internal/db/bundb/migrations/20250224105654_token_app_client_refactor/token.go delete mode 100644 internal/gtsmodel/client.go diff --git a/cmd/gotosocial/action/server/server.go b/cmd/gotosocial/action/server/server.go index dcd30a9b5..0c011510b 100644 --- a/cmd/gotosocial/action/server/server.go +++ b/cmd/gotosocial/action/server/server.go @@ -260,7 +260,7 @@ // Build handlers used in later initializations. mediaManager := media.NewManager(state) - oauthServer := oauth.New(ctx, dbService) + oauthServer := oauth.New(ctx, state, apiutil.GetClientScopeHandler(ctx, state)) typeConverter := typeutils.NewConverter(state) visFilter := visibility.NewFilter(state) intFilter := interaction.NewFilter(state) diff --git a/docs/api/swagger.yaml b/docs/api/swagger.yaml index 563a0c16f..c8b263afe 100644 --- a/docs/api/swagger.yaml +++ b/docs/api/swagger.yaml @@ -843,6 +843,19 @@ definitions: example: https://example.org/callback?some=query type: string x-go-name: RedirectURI + redirect_uris: + description: Post-authorization redirect URIs for the application (OAuth2). + example: '[https://example.org/callback?some=query]' + items: + type: string + type: array + x-go-name: RedirectURIs + scopes: + description: OAuth scopes for this application. + items: + type: string + type: array + x-go-name: Scopes vapid_key: description: Push API key for this application. type: string @@ -7442,16 +7455,17 @@ paths: type: string x-go-name: ClientName - description: |- - Where the user should be redirected after authorization. + Single redirect URI or newline-separated list of redirect URIs (optional). To display the authorization code to the user instead of redirecting to a web page, use `urn:ietf:wg:oauth:2.0:oob` in this parameter. + + If no redirect URIs are provided, defaults to `urn:ietf:wg:oauth:2.0:oob`. in: formData name: redirect_uris - required: true type: string x-go-name: RedirectURIs - description: |- - Space separated list of scopes. + Space separated list of scopes (optional). If no scopes are provided, defaults to `read`. in: formData diff --git a/internal/api/activitypub/users/user_test.go b/internal/api/activitypub/users/user_test.go index d66fe8cf9..c57d9f8c4 100644 --- a/internal/api/activitypub/users/user_test.go +++ b/internal/api/activitypub/users/user_test.go @@ -50,7 +50,6 @@ type UserStandardTestSuite struct { // standard suite models testTokens map[string]*gtsmodel.Token - testClients map[string]*gtsmodel.Client testApplications map[string]*gtsmodel.Application testUsers map[string]*gtsmodel.User testAccounts map[string]*gtsmodel.Account @@ -67,7 +66,6 @@ type UserStandardTestSuite struct { func (suite *UserStandardTestSuite) SetupSuite() { suite.testTokens = testrig.NewTestTokens() - suite.testClients = testrig.NewTestClients() suite.testApplications = testrig.NewTestApplications() suite.testUsers = testrig.NewTestUsers() suite.testAccounts = testrig.NewTestAccounts() diff --git a/internal/api/auth/auth_test.go b/internal/api/auth/auth_test.go index cfbdec7ec..3bf3ec593 100644 --- a/internal/api/auth/auth_test.go +++ b/internal/api/auth/auth_test.go @@ -55,7 +55,6 @@ type AuthStandardTestSuite struct { // standard suite models testTokens map[string]*gtsmodel.Token - testClients map[string]*gtsmodel.Client testApplications map[string]*gtsmodel.Application testUsers map[string]*gtsmodel.User testAccounts map[string]*gtsmodel.Account @@ -71,7 +70,6 @@ type AuthStandardTestSuite struct { func (suite *AuthStandardTestSuite) SetupSuite() { suite.testTokens = testrig.NewTestTokens() - suite.testClients = testrig.NewTestClients() suite.testApplications = testrig.NewTestApplications() suite.testUsers = testrig.NewTestUsers() suite.testAccounts = testrig.NewTestAccounts() diff --git a/internal/api/auth/token_test.go b/internal/api/auth/token_test.go index 1c53b5b2e..1a12fe37e 100644 --- a/internal/api/auth/token_test.go +++ b/internal/api/auth/token_test.go @@ -20,7 +20,7 @@ import ( "context" "encoding/json" - "io/ioutil" + "io" "net/http" "testing" "time" @@ -47,21 +47,21 @@ func (suite *TokenTestSuite) TestPOSTTokenEmptyForm() { result := recorder.Result() defer result.Body.Close() - b, err := ioutil.ReadAll(result.Body) + b, err := io.ReadAll(result.Body) suite.NoError(err) suite.Equal(`{"error":"invalid_request","error_description":"Bad Request: grant_type was not set in the token request form, but must be set to authorization_code or client_credentials: client_id was not set in the token request form: client_secret was not set in the token request form: redirect_uri was not set in the token request form"}`, string(b)) } func (suite *TokenTestSuite) TestRetrieveClientCredentialsOK() { - testClient := suite.testClients["local_account_1"] + testApp := suite.testApplications["application_1"] requestBody, w, err := testrig.CreateMultipartFormData( nil, map[string][]string{ "grant_type": {"client_credentials"}, - "client_id": {testClient.ID}, - "client_secret": {testClient.Secret}, + "client_id": {testApp.ClientID}, + "client_secret": {testApp.ClientSecret}, "redirect_uri": {"http://localhost:8080"}, }) if err != nil { @@ -79,7 +79,7 @@ func (suite *TokenTestSuite) TestRetrieveClientCredentialsOK() { result := recorder.Result() defer result.Body.Close() - b, err := ioutil.ReadAll(result.Body) + b, err := io.ReadAll(result.Body) suite.NoError(err) t := &apimodel.Token{} @@ -98,16 +98,81 @@ func (suite *TokenTestSuite) TestRetrieveClientCredentialsOK() { suite.NotNil(dbToken) } +func (suite *TokenTestSuite) TestRetrieveClientCredentialsBadScope() { + testApp := suite.testApplications["application_1"] + + requestBody, w, err := testrig.CreateMultipartFormData( + nil, + map[string][]string{ + "grant_type": {"client_credentials"}, + "client_id": {testApp.ClientID}, + "client_secret": {testApp.ClientSecret}, + "redirect_uri": {"http://localhost:8080"}, + "scope": {"admin"}, + }) + if err != nil { + panic(err) + } + bodyBytes := requestBody.Bytes() + + ctx, recorder := suite.newContext(http.MethodPost, "oauth/token", bodyBytes, w.FormDataContentType()) + ctx.Request.Header.Set("accept", "application/json") + + suite.authModule.TokenPOSTHandler(ctx) + + suite.Equal(http.StatusForbidden, recorder.Code) + + result := recorder.Result() + defer result.Body.Close() + + b, err := io.ReadAll(result.Body) + suite.NoError(err) + + suite.Equal(`{"error":"invalid_scope","error_description":"Forbidden: requested scope admin was not covered by client scope: If you arrived at this error during a sign in/oauth flow, please try clearing your session cookies and signing in again; if problems persist, make sure you're using the correct credentials"}`, string(b)) +} + +func (suite *TokenTestSuite) TestRetrieveClientCredentialsDifferentRedirectURI() { + testApp := suite.testApplications["application_1"] + + requestBody, w, err := testrig.CreateMultipartFormData( + nil, + map[string][]string{ + "grant_type": {"client_credentials"}, + "client_id": {testApp.ClientID}, + "client_secret": {testApp.ClientSecret}, + "redirect_uri": {"http://somewhere.else.example.org"}, + }) + if err != nil { + panic(err) + } + bodyBytes := requestBody.Bytes() + + ctx, recorder := suite.newContext(http.MethodPost, "oauth/token", bodyBytes, w.FormDataContentType()) + ctx.Request.Header.Set("accept", "application/json") + + suite.authModule.TokenPOSTHandler(ctx) + + suite.Equal(http.StatusForbidden, recorder.Code) + + result := recorder.Result() + defer result.Body.Close() + + b, err := io.ReadAll(result.Body) + suite.NoError(err) + + suite.Equal(`{"error":"invalid redirect uri","error_description":"Forbidden: requested redirect URI http://somewhere.else.example.org was not covered by client redirect URIs: If you arrived at this error during a sign in/oauth flow, please try clearing your session cookies and signing in again; if problems persist, make sure you're using the correct credentials"}`, string(b)) +} + func (suite *TokenTestSuite) TestRetrieveAuthorizationCodeOK() { - testClient := suite.testClients["local_account_1"] + testApp := suite.testApplications["application_1"] testUserAuthorizationToken := suite.testTokens["local_account_1_user_authorization_token"] requestBody, w, err := testrig.CreateMultipartFormData( nil, map[string][]string{ "grant_type": {"authorization_code"}, - "client_id": {testClient.ID}, - "client_secret": {testClient.Secret}, + "client_id": {testApp.ClientID}, + "client_secret": {testApp.ClientSecret}, "redirect_uri": {"http://localhost:8080"}, "code": {testUserAuthorizationToken.Code}, }) @@ -126,7 +191,7 @@ func (suite *TokenTestSuite) TestRetrieveAuthorizationCodeOK() { result := recorder.Result() defer result.Body.Close() - b, err := ioutil.ReadAll(result.Body) + b, err := io.ReadAll(result.Body) suite.NoError(err) t := &apimodel.Token{} @@ -145,14 +210,14 @@ func (suite *TokenTestSuite) TestRetrieveAuthorizationCodeOK() { } func (suite *TokenTestSuite) TestRetrieveAuthorizationCodeNoCode() { - testClient := suite.testClients["local_account_1"] + testApp := suite.testApplications["application_1"] requestBody, w, err := testrig.CreateMultipartFormData( nil, map[string][]string{ "grant_type": {"authorization_code"}, - "client_id": {testClient.ID}, - "client_secret": {testClient.Secret}, + "client_id": {testApp.ClientID}, + "client_secret": {testApp.ClientSecret}, "redirect_uri": {"http://localhost:8080"}, }) if err != nil { @@ -170,21 +235,21 @@ func (suite *TokenTestSuite) TestRetrieveAuthorizationCodeNoCode() { result := recorder.Result() defer result.Body.Close() - b, err := ioutil.ReadAll(result.Body) + b, err := io.ReadAll(result.Body) suite.NoError(err) suite.Equal(`{"error":"invalid_request","error_description":"Bad Request: code was not set in the token request form, but must be set since grant_type is authorization_code"}`, string(b)) } func (suite *TokenTestSuite) TestRetrieveAuthorizationCodeWrongGrantType() { - testClient := suite.testClients["local_account_1"] + testApplication := suite.testApplications["application_1"] requestBody, w, err := testrig.CreateMultipartFormData( nil, map[string][]string{ "grant_type": {"client_credentials"}, - "client_id": {testClient.ID}, - "client_secret": {testClient.Secret}, + "client_id": {testApplication.ClientID}, + "client_secret": {testApplication.ClientSecret}, "redirect_uri": {"http://localhost:8080"}, "code": {"peepeepoopoo"}, }) @@ -203,7 +268,7 @@ func (suite *TokenTestSuite) TestRetrieveAuthorizationCodeWrongGrantType() { result := recorder.Result() defer result.Body.Close() - b, err := ioutil.ReadAll(result.Body) + b, err := io.ReadAll(result.Body) suite.NoError(err) suite.Equal(`{"error":"invalid_request","error_description":"Bad Request: a code was provided in the token request form, but grant_type was not set to authorization_code"}`, string(b)) diff --git a/internal/api/client/accounts/account_test.go b/internal/api/client/accounts/account_test.go index e700ade78..3daa71c91 100644 --- a/internal/api/client/accounts/account_test.go +++ b/internal/api/client/accounts/account_test.go @@ -56,7 +56,6 @@ type AccountStandardTestSuite struct { // standard suite models testTokens map[string]*gtsmodel.Token - testClients map[string]*gtsmodel.Client testApplications map[string]*gtsmodel.Application testUsers map[string]*gtsmodel.User testAccounts map[string]*gtsmodel.Account @@ -69,7 +68,6 @@ type AccountStandardTestSuite struct { func (suite *AccountStandardTestSuite) SetupSuite() { suite.testTokens = testrig.NewTestTokens() - suite.testClients = testrig.NewTestClients() suite.testApplications = testrig.NewTestApplications() suite.testUsers = testrig.NewTestUsers() suite.testAccounts = testrig.NewTestAccounts() diff --git a/internal/api/client/admin/admin_test.go b/internal/api/client/admin/admin_test.go index f44d48d78..6bc777119 100644 --- a/internal/api/client/admin/admin_test.go +++ b/internal/api/client/admin/admin_test.go @@ -56,7 +56,6 @@ type AdminStandardTestSuite struct { // standard suite models testTokens map[string]*gtsmodel.Token - testClients map[string]*gtsmodel.Client testApplications map[string]*gtsmodel.Application testUsers map[string]*gtsmodel.User testAccounts map[string]*gtsmodel.Account @@ -72,7 +71,6 @@ type AdminStandardTestSuite struct { func (suite *AdminStandardTestSuite) SetupSuite() { suite.testTokens = testrig.NewTestTokens() - suite.testClients = testrig.NewTestClients() suite.testApplications = testrig.NewTestApplications() suite.testUsers = testrig.NewTestUsers() suite.testAccounts = testrig.NewTestAccounts() diff --git a/internal/api/client/bookmarks/bookmarks_test.go b/internal/api/client/bookmarks/bookmarks_test.go index a11597f7c..3608078b9 100644 --- a/internal/api/client/bookmarks/bookmarks_test.go +++ b/internal/api/client/bookmarks/bookmarks_test.go @@ -61,7 +61,6 @@ type BookmarkTestSuite struct { // standard suite models testTokens map[string]*gtsmodel.Token - testClients map[string]*gtsmodel.Client testApplications map[string]*gtsmodel.Application testUsers map[string]*gtsmodel.User testAccounts map[string]*gtsmodel.Account @@ -77,7 +76,6 @@ type BookmarkTestSuite struct { func (suite *BookmarkTestSuite) SetupSuite() { suite.testTokens = testrig.NewTestTokens() - suite.testClients = testrig.NewTestClients() suite.testApplications = testrig.NewTestApplications() suite.testUsers = testrig.NewTestUsers() suite.testAccounts = testrig.NewTestAccounts() diff --git a/internal/api/client/exports/exports_test.go b/internal/api/client/exports/exports_test.go index 55d873348..6fbeb57d0 100644 --- a/internal/api/client/exports/exports_test.go +++ b/internal/api/client/exports/exports_test.go @@ -44,7 +44,6 @@ type ExportsTestSuite struct { // standard suite models testTokens map[string]*gtsmodel.Token - testClients map[string]*gtsmodel.Client testApplications map[string]*gtsmodel.Application testUsers map[string]*gtsmodel.User testAccounts map[string]*gtsmodel.Account @@ -55,7 +54,6 @@ type ExportsTestSuite struct { func (suite *ExportsTestSuite) SetupSuite() { suite.testTokens = testrig.NewTestTokens() - suite.testClients = testrig.NewTestClients() suite.testApplications = testrig.NewTestApplications() suite.testUsers = testrig.NewTestUsers() suite.testAccounts = testrig.NewTestAccounts() diff --git a/internal/api/client/favourites/favourites_test.go b/internal/api/client/favourites/favourites_test.go index 7cfa205e3..7c65e4b97 100644 --- a/internal/api/client/favourites/favourites_test.go +++ b/internal/api/client/favourites/favourites_test.go @@ -48,7 +48,6 @@ type FavouritesStandardTestSuite struct { // standard suite models testTokens map[string]*gtsmodel.Token - testClients map[string]*gtsmodel.Client testApplications map[string]*gtsmodel.Application testUsers map[string]*gtsmodel.User testAccounts map[string]*gtsmodel.Account @@ -62,7 +61,6 @@ type FavouritesStandardTestSuite struct { func (suite *FavouritesStandardTestSuite) SetupSuite() { suite.testTokens = testrig.NewTestTokens() - suite.testClients = testrig.NewTestClients() suite.testApplications = testrig.NewTestApplications() suite.testUsers = testrig.NewTestUsers() suite.testAccounts = testrig.NewTestAccounts() diff --git a/internal/api/client/filters/v1/filter_test.go b/internal/api/client/filters/v1/filter_test.go index 558f3d959..e0bcf8731 100644 --- a/internal/api/client/filters/v1/filter_test.go +++ b/internal/api/client/filters/v1/filter_test.go @@ -53,7 +53,6 @@ type FiltersTestSuite struct { // standard suite models testTokens map[string]*gtsmodel.Token - testClients map[string]*gtsmodel.Client testApplications map[string]*gtsmodel.Application testUsers map[string]*gtsmodel.User testAccounts map[string]*gtsmodel.Account @@ -68,7 +67,6 @@ type FiltersTestSuite struct { func (suite *FiltersTestSuite) SetupSuite() { suite.testTokens = testrig.NewTestTokens() - suite.testClients = testrig.NewTestClients() suite.testApplications = testrig.NewTestApplications() suite.testUsers = testrig.NewTestUsers() suite.testAccounts = testrig.NewTestAccounts() diff --git a/internal/api/client/filters/v2/filter_test.go b/internal/api/client/filters/v2/filter_test.go index 8301c67ad..af212ac88 100644 --- a/internal/api/client/filters/v2/filter_test.go +++ b/internal/api/client/filters/v2/filter_test.go @@ -53,7 +53,6 @@ type FiltersTestSuite struct { // standard suite models testTokens map[string]*gtsmodel.Token - testClients map[string]*gtsmodel.Client testApplications map[string]*gtsmodel.Application testUsers map[string]*gtsmodel.User testAccounts map[string]*gtsmodel.Account @@ -68,7 +67,6 @@ type FiltersTestSuite struct { func (suite *FiltersTestSuite) SetupSuite() { suite.testTokens = testrig.NewTestTokens() - suite.testClients = testrig.NewTestClients() suite.testApplications = testrig.NewTestApplications() suite.testUsers = testrig.NewTestUsers() suite.testAccounts = testrig.NewTestAccounts() diff --git a/internal/api/client/followedtags/followedtags_test.go b/internal/api/client/followedtags/followedtags_test.go index 816e1d0cc..e7c83ca68 100644 --- a/internal/api/client/followedtags/followedtags_test.go +++ b/internal/api/client/followedtags/followedtags_test.go @@ -48,7 +48,6 @@ type FollowedTagsTestSuite struct { // standard suite models testTokens map[string]*gtsmodel.Token - testClients map[string]*gtsmodel.Client testApplications map[string]*gtsmodel.Application testUsers map[string]*gtsmodel.User testAccounts map[string]*gtsmodel.Account @@ -60,7 +59,6 @@ type FollowedTagsTestSuite struct { func (suite *FollowedTagsTestSuite) SetupSuite() { suite.testTokens = testrig.NewTestTokens() - suite.testClients = testrig.NewTestClients() suite.testApplications = testrig.NewTestApplications() suite.testUsers = testrig.NewTestUsers() suite.testAccounts = testrig.NewTestAccounts() diff --git a/internal/api/client/followrequests/followrequest_test.go b/internal/api/client/followrequests/followrequest_test.go index 787d47c84..fbaf9a560 100644 --- a/internal/api/client/followrequests/followrequest_test.go +++ b/internal/api/client/followrequests/followrequest_test.go @@ -53,7 +53,6 @@ type FollowRequestStandardTestSuite struct { // standard suite models testTokens map[string]*gtsmodel.Token - testClients map[string]*gtsmodel.Client testApplications map[string]*gtsmodel.Application testUsers map[string]*gtsmodel.User testAccounts map[string]*gtsmodel.Account @@ -66,7 +65,6 @@ type FollowRequestStandardTestSuite struct { func (suite *FollowRequestStandardTestSuite) SetupSuite() { suite.testTokens = testrig.NewTestTokens() - suite.testClients = testrig.NewTestClients() suite.testApplications = testrig.NewTestApplications() suite.testUsers = testrig.NewTestUsers() suite.testAccounts = testrig.NewTestAccounts() diff --git a/internal/api/client/import/import_test.go b/internal/api/client/import/import_test.go index 56497d27d..1edb54b64 100644 --- a/internal/api/client/import/import_test.go +++ b/internal/api/client/import/import_test.go @@ -43,7 +43,6 @@ type ImportTestSuite struct { // standard suite models testTokens map[string]*gtsmodel.Token - testClients map[string]*gtsmodel.Client testApplications map[string]*gtsmodel.Application testUsers map[string]*gtsmodel.User testAccounts map[string]*gtsmodel.Account @@ -54,7 +53,6 @@ type ImportTestSuite struct { func (suite *ImportTestSuite) SetupSuite() { suite.testTokens = testrig.NewTestTokens() - suite.testClients = testrig.NewTestClients() suite.testApplications = testrig.NewTestApplications() suite.testUsers = testrig.NewTestUsers() suite.testAccounts = testrig.NewTestAccounts() diff --git a/internal/api/client/instance/instance_test.go b/internal/api/client/instance/instance_test.go index f0427369b..965d09609 100644 --- a/internal/api/client/instance/instance_test.go +++ b/internal/api/client/instance/instance_test.go @@ -55,7 +55,6 @@ type InstanceStandardTestSuite struct { // standard suite models testTokens map[string]*gtsmodel.Token - testClients map[string]*gtsmodel.Client testApplications map[string]*gtsmodel.Application testUsers map[string]*gtsmodel.User testAccounts map[string]*gtsmodel.Account @@ -68,7 +67,6 @@ type InstanceStandardTestSuite struct { func (suite *InstanceStandardTestSuite) SetupSuite() { suite.testTokens = testrig.NewTestTokens() - suite.testClients = testrig.NewTestClients() suite.testApplications = testrig.NewTestApplications() suite.testUsers = testrig.NewTestUsers() suite.testAccounts = testrig.NewTestAccounts() diff --git a/internal/api/client/lists/lists_test.go b/internal/api/client/lists/lists_test.go index 5fd2304c7..65242db25 100644 --- a/internal/api/client/lists/lists_test.go +++ b/internal/api/client/lists/lists_test.go @@ -47,7 +47,6 @@ type ListsStandardTestSuite struct { // standard suite models testTokens map[string]*gtsmodel.Token - testClients map[string]*gtsmodel.Client testApplications map[string]*gtsmodel.Application testUsers map[string]*gtsmodel.User testAccounts map[string]*gtsmodel.Account @@ -64,7 +63,6 @@ type ListsStandardTestSuite struct { func (suite *ListsStandardTestSuite) SetupSuite() { suite.testTokens = testrig.NewTestTokens() - suite.testClients = testrig.NewTestClients() suite.testApplications = testrig.NewTestApplications() suite.testUsers = testrig.NewTestUsers() suite.testAccounts = testrig.NewTestAccounts() diff --git a/internal/api/client/media/mediacreate_test.go b/internal/api/client/media/mediacreate_test.go index d26f2bb7a..fabff595b 100644 --- a/internal/api/client/media/mediacreate_test.go +++ b/internal/api/client/media/mediacreate_test.go @@ -62,7 +62,6 @@ type MediaCreateTestSuite struct { // standard suite models testTokens map[string]*gtsmodel.Token - testClients map[string]*gtsmodel.Client testApplications map[string]*gtsmodel.Application testUsers map[string]*gtsmodel.User testAccounts map[string]*gtsmodel.Account @@ -101,7 +100,7 @@ func (suite *MediaCreateTestSuite) SetupTest() { ) suite.mediaManager = testrig.NewTestMediaManager(&suite.state) - suite.oauthServer = testrig.NewTestOauthServer(suite.db) + suite.oauthServer = testrig.NewTestOauthServer(&suite.state) suite.federator = testrig.NewTestFederator(&suite.state, testrig.NewTestTransportController(&suite.state, testrig.NewMockHTTPClient(nil, "../../../../testrig/media")), suite.mediaManager) suite.emailSender = testrig.NewEmailSender("../../../../web/template/", nil) suite.processor = testrig.NewTestProcessor( @@ -117,7 +116,6 @@ func (suite *MediaCreateTestSuite) SetupTest() { // setup test data suite.testTokens = testrig.NewTestTokens() - suite.testClients = testrig.NewTestClients() suite.testApplications = testrig.NewTestApplications() suite.testUsers = testrig.NewTestUsers() suite.testAccounts = testrig.NewTestAccounts() diff --git a/internal/api/client/media/mediaupdate_test.go b/internal/api/client/media/mediaupdate_test.go index dd115f465..8e033f367 100644 --- a/internal/api/client/media/mediaupdate_test.go +++ b/internal/api/client/media/mediaupdate_test.go @@ -60,7 +60,6 @@ type MediaUpdateTestSuite struct { // standard suite models testTokens map[string]*gtsmodel.Token - testClients map[string]*gtsmodel.Client testApplications map[string]*gtsmodel.Application testUsers map[string]*gtsmodel.User testAccounts map[string]*gtsmodel.Account @@ -99,7 +98,7 @@ func (suite *MediaUpdateTestSuite) SetupTest() { ) suite.mediaManager = testrig.NewTestMediaManager(&suite.state) - suite.oauthServer = testrig.NewTestOauthServer(suite.db) + suite.oauthServer = testrig.NewTestOauthServer(&suite.state) suite.federator = testrig.NewTestFederator(&suite.state, testrig.NewTestTransportController(&suite.state, testrig.NewMockHTTPClient(nil, "../../../../testrig/media")), suite.mediaManager) suite.emailSender = testrig.NewEmailSender("../../../../web/template/", nil) suite.processor = testrig.NewTestProcessor( @@ -115,7 +114,6 @@ func (suite *MediaUpdateTestSuite) SetupTest() { // setup test data suite.testTokens = testrig.NewTestTokens() - suite.testClients = testrig.NewTestClients() suite.testApplications = testrig.NewTestApplications() suite.testUsers = testrig.NewTestUsers() suite.testAccounts = testrig.NewTestAccounts() diff --git a/internal/api/client/mutes/mutes_test.go b/internal/api/client/mutes/mutes_test.go index 3f5686cfb..fdfca4414 100644 --- a/internal/api/client/mutes/mutes_test.go +++ b/internal/api/client/mutes/mutes_test.go @@ -56,7 +56,6 @@ type MutesTestSuite struct { // standard suite models testTokens map[string]*gtsmodel.Token - testClients map[string]*gtsmodel.Client testApplications map[string]*gtsmodel.Application testUsers map[string]*gtsmodel.User testAccounts map[string]*gtsmodel.Account @@ -67,7 +66,6 @@ type MutesTestSuite struct { func (suite *MutesTestSuite) SetupSuite() { suite.testTokens = testrig.NewTestTokens() - suite.testClients = testrig.NewTestClients() suite.testApplications = testrig.NewTestApplications() suite.testUsers = testrig.NewTestUsers() suite.testAccounts = testrig.NewTestAccounts() diff --git a/internal/api/client/notifications/notifications_test.go b/internal/api/client/notifications/notifications_test.go index 5794c0e12..b84e7d768 100644 --- a/internal/api/client/notifications/notifications_test.go +++ b/internal/api/client/notifications/notifications_test.go @@ -48,7 +48,6 @@ type NotificationsTestSuite struct { // standard suite models testTokens map[string]*gtsmodel.Token - testClients map[string]*gtsmodel.Client testApplications map[string]*gtsmodel.Application testUsers map[string]*gtsmodel.User testAccounts map[string]*gtsmodel.Account @@ -63,7 +62,6 @@ type NotificationsTestSuite struct { func (suite *NotificationsTestSuite) SetupSuite() { suite.testTokens = testrig.NewTestTokens() - suite.testClients = testrig.NewTestClients() suite.testApplications = testrig.NewTestApplications() suite.testUsers = testrig.NewTestUsers() suite.testAccounts = testrig.NewTestAccounts() diff --git a/internal/api/client/polls/polls_test.go b/internal/api/client/polls/polls_test.go index 8c2bc8ba1..5df5cf88d 100644 --- a/internal/api/client/polls/polls_test.go +++ b/internal/api/client/polls/polls_test.go @@ -48,7 +48,6 @@ type PollsStandardTestSuite struct { // standard suite models testTokens map[string]*gtsmodel.Token - testClients map[string]*gtsmodel.Client testApplications map[string]*gtsmodel.Application testUsers map[string]*gtsmodel.User testAccounts map[string]*gtsmodel.Account @@ -61,7 +60,6 @@ type PollsStandardTestSuite struct { func (suite *PollsStandardTestSuite) SetupSuite() { suite.testTokens = testrig.NewTestTokens() - suite.testClients = testrig.NewTestClients() suite.testApplications = testrig.NewTestApplications() suite.testUsers = testrig.NewTestUsers() suite.testAccounts = testrig.NewTestAccounts() diff --git a/internal/api/client/push/push_test.go b/internal/api/client/push/push_test.go index 0d85192ff..6a3754546 100644 --- a/internal/api/client/push/push_test.go +++ b/internal/api/client/push/push_test.go @@ -47,7 +47,6 @@ type PushTestSuite struct { // standard suite models testTokens map[string]*gtsmodel.Token - testClients map[string]*gtsmodel.Client testApplications map[string]*gtsmodel.Application testUsers map[string]*gtsmodel.User testAccounts map[string]*gtsmodel.Account @@ -59,7 +58,6 @@ type PushTestSuite struct { func (suite *PushTestSuite) SetupSuite() { suite.testTokens = testrig.NewTestTokens() - suite.testClients = testrig.NewTestClients() suite.testApplications = testrig.NewTestApplications() suite.testUsers = testrig.NewTestUsers() suite.testAccounts = testrig.NewTestAccounts() diff --git a/internal/api/client/reports/reports_test.go b/internal/api/client/reports/reports_test.go index 89240a4b1..da39c78e1 100644 --- a/internal/api/client/reports/reports_test.go +++ b/internal/api/client/reports/reports_test.go @@ -47,7 +47,6 @@ type ReportsStandardTestSuite struct { // standard suite models testTokens map[string]*gtsmodel.Token - testClients map[string]*gtsmodel.Client testApplications map[string]*gtsmodel.Application testUsers map[string]*gtsmodel.User testAccounts map[string]*gtsmodel.Account @@ -60,7 +59,6 @@ type ReportsStandardTestSuite struct { func (suite *ReportsStandardTestSuite) SetupSuite() { suite.testTokens = testrig.NewTestTokens() - suite.testClients = testrig.NewTestClients() suite.testApplications = testrig.NewTestApplications() suite.testUsers = testrig.NewTestUsers() suite.testAccounts = testrig.NewTestAccounts() diff --git a/internal/api/client/search/search_test.go b/internal/api/client/search/search_test.go index 219966c7c..9eb7f08fe 100644 --- a/internal/api/client/search/search_test.go +++ b/internal/api/client/search/search_test.go @@ -55,7 +55,6 @@ type SearchStandardTestSuite struct { // standard suite models testTokens map[string]*gtsmodel.Token - testClients map[string]*gtsmodel.Client testApplications map[string]*gtsmodel.Application testUsers map[string]*gtsmodel.User testAccounts map[string]*gtsmodel.Account @@ -66,7 +65,6 @@ type SearchStandardTestSuite struct { func (suite *SearchStandardTestSuite) SetupSuite() { suite.testTokens = testrig.NewTestTokens() - suite.testClients = testrig.NewTestClients() suite.testApplications = testrig.NewTestApplications() suite.testUsers = testrig.NewTestUsers() suite.testAccounts = testrig.NewTestAccounts() diff --git a/internal/api/client/statuses/status_test.go b/internal/api/client/statuses/status_test.go index c5f2838e8..2b916125e 100644 --- a/internal/api/client/statuses/status_test.go +++ b/internal/api/client/statuses/status_test.go @@ -55,7 +55,6 @@ type StatusStandardTestSuite struct { // standard suite models testTokens map[string]*gtsmodel.Token - testClients map[string]*gtsmodel.Client testApplications map[string]*gtsmodel.Application testUsers map[string]*gtsmodel.User testAccounts map[string]*gtsmodel.Account @@ -176,7 +175,6 @@ func (suite *StatusStandardTestSuite) determinateStatus(rawMap map[string]any) { func (suite *StatusStandardTestSuite) SetupSuite() { suite.testTokens = testrig.NewTestTokens() - suite.testClients = testrig.NewTestClients() suite.testApplications = testrig.NewTestApplications() suite.testUsers = testrig.NewTestUsers() suite.testAccounts = testrig.NewTestAccounts() diff --git a/internal/api/client/streaming/streaming_test.go b/internal/api/client/streaming/streaming_test.go index 00ad2de03..4cc5dc1b2 100644 --- a/internal/api/client/streaming/streaming_test.go +++ b/internal/api/client/streaming/streaming_test.go @@ -61,7 +61,6 @@ type StreamingTestSuite struct { // standard suite models testTokens map[string]*gtsmodel.Token - testClients map[string]*gtsmodel.Client testApplications map[string]*gtsmodel.Application testUsers map[string]*gtsmodel.User testAccounts map[string]*gtsmodel.Account @@ -75,7 +74,6 @@ type StreamingTestSuite struct { func (suite *StreamingTestSuite) SetupSuite() { suite.testTokens = testrig.NewTestTokens() - suite.testClients = testrig.NewTestClients() suite.testApplications = testrig.NewTestApplications() suite.testUsers = testrig.NewTestUsers() suite.testAccounts = testrig.NewTestAccounts() diff --git a/internal/api/client/tags/tags_test.go b/internal/api/client/tags/tags_test.go index c24574d47..4718d5f34 100644 --- a/internal/api/client/tags/tags_test.go +++ b/internal/api/client/tags/tags_test.go @@ -56,7 +56,6 @@ type TagsTestSuite struct { // standard suite models testTokens map[string]*gtsmodel.Token - testClients map[string]*gtsmodel.Client testApplications map[string]*gtsmodel.Application testUsers map[string]*gtsmodel.User testAccounts map[string]*gtsmodel.Account @@ -68,7 +67,6 @@ type TagsTestSuite struct { func (suite *TagsTestSuite) SetupSuite() { suite.testTokens = testrig.NewTestTokens() - suite.testClients = testrig.NewTestClients() suite.testApplications = testrig.NewTestApplications() suite.testUsers = testrig.NewTestUsers() suite.testAccounts = testrig.NewTestAccounts() diff --git a/internal/api/client/user/user_test.go b/internal/api/client/user/user_test.go index 8cf359cd8..8f54c82a0 100644 --- a/internal/api/client/user/user_test.go +++ b/internal/api/client/user/user_test.go @@ -50,7 +50,6 @@ type UserStandardTestSuite struct { state state.State testTokens map[string]*gtsmodel.Token - testClients map[string]*gtsmodel.Client testApplications map[string]*gtsmodel.Application testUsers map[string]*gtsmodel.User testAccounts map[string]*gtsmodel.Account @@ -66,7 +65,6 @@ func (suite *UserStandardTestSuite) SetupTest() { testrig.InitTestLog() suite.testTokens = testrig.NewTestTokens() - suite.testClients = testrig.NewTestClients() suite.testApplications = testrig.NewTestApplications() suite.testUsers = testrig.NewTestUsers() suite.testAccounts = testrig.NewTestAccounts() diff --git a/internal/api/fileserver/fileserver_test.go b/internal/api/fileserver/fileserver_test.go index 9b0580e92..9ba647ff3 100644 --- a/internal/api/fileserver/fileserver_test.go +++ b/internal/api/fileserver/fileserver_test.go @@ -51,7 +51,6 @@ type FileserverTestSuite struct { // standard suite models testTokens map[string]*gtsmodel.Token - testClients map[string]*gtsmodel.Client testApplications map[string]*gtsmodel.Application testUsers map[string]*gtsmodel.User testAccounts map[string]*gtsmodel.Account @@ -100,7 +99,7 @@ func (suite *FileserverTestSuite) SetupSuite() { ) suite.mediaManager = testrig.NewTestMediaManager(&suite.state) - suite.oauthServer = testrig.NewTestOauthServer(suite.db) + suite.oauthServer = testrig.NewTestOauthServer(&suite.state) suite.emailSender = testrig.NewEmailSender("../../../web/template/", nil) suite.fileServer = fileserver.New(suite.processor) @@ -118,7 +117,6 @@ func (suite *FileserverTestSuite) SetupTest() { testrig.StandardStorageSetup(suite.storage, "../../../testrig/media") suite.testTokens = testrig.NewTestTokens() - suite.testClients = testrig.NewTestClients() suite.testApplications = testrig.NewTestApplications() suite.testUsers = testrig.NewTestUsers() suite.testAccounts = testrig.NewTestAccounts() diff --git a/internal/api/model/application.go b/internal/api/model/application.go index 0770772b7..720674ad5 100644 --- a/internal/api/model/application.go +++ b/internal/api/model/application.go @@ -33,12 +33,17 @@ type Application struct { // Post-authorization redirect URI for the application (OAuth2). // example: https://example.org/callback?some=query RedirectURI string `json:"redirect_uri,omitempty"` + // Post-authorization redirect URIs for the application (OAuth2). + // example: [https://example.org/callback?some=query] + RedirectURIs []string `json:"redirect_uris,omitempty"` // Client ID associated with this application. ClientID string `json:"client_id,omitempty"` // Client secret associated with this application. ClientSecret string `json:"client_secret,omitempty"` // Push API key for this application. VapidKey string `json:"vapid_key,omitempty"` + // OAuth scopes for this application. + Scopes []string `json:"scopes,omitempty"` } // ApplicationCreateRequest models app create parameters. @@ -50,14 +55,15 @@ type ApplicationCreateRequest struct { // in: formData // required: true ClientName string `form:"client_name" json:"client_name" xml:"client_name" binding:"required"` - // Where the user should be redirected after authorization. + // Single redirect URI or newline-separated list of redirect URIs (optional). // // To display the authorization code to the user instead of redirecting to a web page, use `urn:ietf:wg:oauth:2.0:oob` in this parameter. // + // If no redirect URIs are provided, defaults to `urn:ietf:wg:oauth:2.0:oob`. + // // in: formData - // required: true - RedirectURIs string `form:"redirect_uris" json:"redirect_uris" xml:"redirect_uris" binding:"required"` - // Space separated list of scopes. + RedirectURIs string `form:"redirect_uris" json:"redirect_uris" xml:"redirect_uris"` + // Space separated list of scopes (optional). // // If no scopes are provided, defaults to `read`. // diff --git a/internal/api/util/auth.go b/internal/api/util/auth.go index fccdf38e1..b56827998 100644 --- a/internal/api/util/auth.go +++ b/internal/api/util/auth.go @@ -18,15 +18,21 @@ package util import ( + "context" "errors" "slices" "strings" "codeberg.org/superseriousbusiness/oauth2/v4" + "codeberg.org/superseriousbusiness/oauth2/v4/server" "github.com/gin-gonic/gin" + "github.com/superseriousbusiness/gotosocial/internal/db" + "github.com/superseriousbusiness/gotosocial/internal/gtscontext" "github.com/superseriousbusiness/gotosocial/internal/gtserror" "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" + "github.com/superseriousbusiness/gotosocial/internal/log" "github.com/superseriousbusiness/gotosocial/internal/oauth" + "github.com/superseriousbusiness/gotosocial/internal/state" ) // Auth wraps an authorized token, application, user, and account. @@ -150,3 +156,51 @@ func(hasScope string) bool { return a, nil } + +// GetClientScopeHandler returns a handler for testing scope on a TokenGenerateRequest. +func GetClientScopeHandler(ctx context.Context, state *state.State) server.ClientScopeHandler { + return func(tgr *oauth2.TokenGenerateRequest) (allowed bool, err error) { + application, err := state.DB.GetApplicationByClientID( + gtscontext.SetBarebones(ctx), + tgr.ClientID, + ) + if err != nil && !errors.Is(err, db.ErrNoEntries) { + log.Errorf(ctx, "database error getting application: %v", err) + return false, err + } + + if application == nil { + err := gtserror.Newf("no application found with client id %s", tgr.ClientID) + return false, err + } + + // Normalize scope. + if strings.TrimSpace(tgr.Scope) == "" { + tgr.Scope = "read" + } + + // Make sure requested scopes are all + // within scopes permitted by application. + hasScopes := strings.Split(application.Scopes, " ") + wantsScopes := strings.Split(tgr.Scope, " ") + for _, wantsScope := range wantsScopes { + thisOK := slices.ContainsFunc( + hasScopes, + func(hasScope string) bool { + has := Scope(hasScope) + wants := Scope(wantsScope) + return has.Permits(wants) + }, + ) + + if !thisOK { + // Requested unpermitted + // scope for this app. + return false, nil + } + } + + // All OK. + return true, nil + } +} diff --git a/internal/api/util/scopes.go b/internal/api/util/scopes.go index d02d3cc0d..8161de500 100644 --- a/internal/api/util/scopes.go +++ b/internal/api/util/scopes.go @@ -93,11 +93,29 @@ // scope permits the wanted scope. func (has Scope) Permits(wanted Scope) bool { if has == wanted { - // Exact match. + // Exact match on either a + // top-level or granular scope. return true } - // Check if we have a parent scope of what's wanted, - // eg., we have scope "admin", we want "admin:read". - return strings.HasPrefix(string(wanted), string(has)) + // Ensure we have a + // known top-level scope. + switch has { + + case ScopeProfile, + ScopePush, + ScopeRead, + ScopeWrite, + ScopeAdmin, + ScopeAdminRead, + ScopeAdminWrite: + // Check if top-level includes wanted, + // eg., have "admin", want "admin:read". + return strings.HasPrefix(string(wanted), string(has)+":") + + default: + // Unknown top-level scope, + // can't permit anything. + return false + } } diff --git a/internal/api/util/scopes_test.go b/internal/api/util/scopes_test.go index bd533585b..72f6b57aa 100644 --- a/internal/api/util/scopes_test.go +++ b/internal/api/util/scopes_test.go @@ -89,6 +89,16 @@ func TestScopes(t *testing.T) { WantsScope: util.ScopeWrite, Expect: false, }, + { + HasScope: util.ScopeProfile, + WantsScope: util.ScopePush, + Expect: false, + }, + { + HasScope: util.Scope("p"), + WantsScope: util.ScopePush, + Expect: false, + }, } { res := test.HasScope.Permits(test.WantsScope) if res != test.Expect { diff --git a/internal/api/wellknown/webfinger/webfinger_test.go b/internal/api/wellknown/webfinger/webfinger_test.go index 234c1ad16..d6521aff0 100644 --- a/internal/api/wellknown/webfinger/webfinger_test.go +++ b/internal/api/wellknown/webfinger/webfinger_test.go @@ -50,7 +50,6 @@ type WebfingerStandardTestSuite struct { // standard suite models testTokens map[string]*gtsmodel.Token - testClients map[string]*gtsmodel.Client testApplications map[string]*gtsmodel.Application testUsers map[string]*gtsmodel.User testAccounts map[string]*gtsmodel.Account @@ -63,7 +62,6 @@ type WebfingerStandardTestSuite struct { func (suite *WebfingerStandardTestSuite) SetupSuite() { suite.testTokens = testrig.NewTestTokens() - suite.testClients = testrig.NewTestClients() suite.testApplications = testrig.NewTestApplications() suite.testUsers = testrig.NewTestUsers() suite.testAccounts = testrig.NewTestAccounts() @@ -102,7 +100,7 @@ func (suite *WebfingerStandardTestSuite) SetupTest() { suite.mediaManager, ) suite.webfingerModule = webfinger.New(suite.processor) - suite.oauthServer = testrig.NewTestOauthServer(suite.db) + suite.oauthServer = testrig.NewTestOauthServer(&suite.state) testrig.StandardDBSetup(suite.db, suite.testAccounts) testrig.StandardStorageSetup(suite.storage, "../../../../testrig/media") } diff --git a/internal/api/wellknown/webfinger/webfingerget_test.go b/internal/api/wellknown/webfinger/webfingerget_test.go index 4bb6f323d..1707584a5 100644 --- a/internal/api/wellknown/webfinger/webfingerget_test.go +++ b/internal/api/wellknown/webfinger/webfingerget_test.go @@ -94,7 +94,7 @@ func (suite *WebfingerGetTestSuite) funkifyAccountDomain(host string, accountDom subscriptions.New(&suite.state, suite.federator.TransportController(), suite.tc), suite.tc, suite.federator, - testrig.NewTestOauthServer(suite.db), + testrig.NewTestOauthServer(&suite.state), testrig.NewTestMediaManager(&suite.state), &suite.state, suite.emailSender, diff --git a/internal/cache/cache.go b/internal/cache/cache.go index 5771b4e95..e57fbb569 100644 --- a/internal/cache/cache.go +++ b/internal/cache/cache.go @@ -69,7 +69,6 @@ func (c *Caches) Init() { c.initBlock() c.initBlockIDs() c.initBoostOfIDs() - c.initClient() c.initConversation() c.initConversationLastStatusIDs() c.initDomainAllow() @@ -161,7 +160,6 @@ func (c *Caches) Sweep(threshold float64) { c.DB.Block.Trim(threshold) c.DB.BlockIDs.Trim(threshold) c.DB.BoostOfIDs.Trim(threshold) - c.DB.Client.Trim(threshold) c.DB.Conversation.Trim(threshold) c.DB.ConversationLastStatusIDs.Trim(threshold) c.DB.Emoji.Trim(threshold) diff --git a/internal/cache/db.go b/internal/cache/db.go index 180d81907..695b19b8f 100644 --- a/internal/cache/db.go +++ b/internal/cache/db.go @@ -52,9 +52,6 @@ type DBCaches struct { // BoostOfIDs provides access to the boost of IDs list database cache. BoostOfIDs SliceCache[string] - // Client provides access to the gtsmodel Client database cache. - Client StructCache[*gtsmodel.Client] - // Conversation provides access to the gtsmodel Conversation database cache. Conversation StructCache[*gtsmodel.Conversation] @@ -489,32 +486,6 @@ func (c *Caches) initBoostOfIDs() { c.DB.BoostOfIDs.Init(0, cap) } -func (c *Caches) initClient() { - // Calculate maximum cache size. - cap := calculateResultCacheMax( - sizeofClient(), // model in-mem size. - config.GetCacheClientMemRatio(), - ) - - log.Infof(nil, "cache size = %d", cap) - - copyF := func(c1 *gtsmodel.Client) *gtsmodel.Client { - c2 := new(gtsmodel.Client) - *c2 = *c1 - return c2 - } - - c.DB.Client.Init(structr.CacheConfig[*gtsmodel.Client]{ - Indices: []structr.IndexConfig{ - {Fields: "ID"}, - }, - MaxSize: cap, - IgnoreErr: ignoreErrors, - Copy: copyF, - Invalidate: c.OnInvalidateClient, - }) -} - func (c *Caches) initConversation() { cap := calculateResultCacheMax( sizeofConversation(), // model in-mem size. diff --git a/internal/cache/invalidate.go b/internal/cache/invalidate.go index 555c73cd7..949238ec6 100644 --- a/internal/cache/invalidate.go +++ b/internal/cache/invalidate.go @@ -62,8 +62,7 @@ func (c *Caches) OnInvalidateAccount(account *gtsmodel.Account) { } func (c *Caches) OnInvalidateApplication(app *gtsmodel.Application) { - // Invalidate cached client of this application. - c.DB.Client.Invalidate("ID", app.ClientID) + // TODO: invalidate tokens? } func (c *Caches) OnInvalidateBlock(block *gtsmodel.Block) { @@ -79,11 +78,6 @@ func (c *Caches) OnInvalidateBlock(block *gtsmodel.Block) { c.DB.BlockIDs.Invalidate(block.AccountID) } -func (c *Caches) OnInvalidateClient(client *gtsmodel.Client) { - // Invalidate any tokens under this client. - c.DB.Token.Invalidate("ClientID", client.ID) -} - func (c *Caches) OnInvalidateConversation(conversation *gtsmodel.Conversation) { // Invalidate owning account's conversation list. c.DB.ConversationLastStatusIDs.Invalidate(conversation.AccountID) diff --git a/internal/cache/size.go b/internal/cache/size.go index c96a3cd2e..1c8c5fe2e 100644 --- a/internal/cache/size.go +++ b/internal/cache/size.go @@ -302,15 +302,14 @@ func sizeofAccountStats() uintptr { func sizeofApplication() uintptr { return uintptr(size.Of(>smodel.Application{ - ID: exampleID, - CreatedAt: exampleTime, - UpdatedAt: exampleTime, - Name: exampleUsername, - Website: exampleURI, - RedirectURI: exampleURI, - ClientID: exampleID, - ClientSecret: exampleID, - Scopes: exampleTextSmall, + ID: exampleID, + Name: exampleUsername, + Website: exampleURI, + RedirectURIs: []string{exampleURI}, + ClientID: exampleID, + ClientSecret: exampleID, + Scopes: exampleTextSmall, + ManagedByUserID: exampleID, })) } @@ -325,17 +324,6 @@ func sizeofBlock() uintptr { })) } -func sizeofClient() uintptr { - return uintptr(size.Of(>smodel.Client{ - ID: exampleID, - CreatedAt: exampleTime, - UpdatedAt: exampleTime, - Secret: exampleID, - Domain: exampleURI, - UserID: exampleID, - })) -} - func sizeofConversation() uintptr { return uintptr(size.Of(>smodel.Conversation{ ID: exampleID, @@ -752,8 +740,7 @@ func sizeofThreadMute() uintptr { func sizeofToken() uintptr { return uintptr(size.Of(>smodel.Token{ ID: exampleID, - CreatedAt: exampleTime, - UpdatedAt: exampleTime, + LastUsed: exampleTime, ClientID: exampleID, UserID: exampleID, RedirectURI: exampleURI, diff --git a/internal/db/application.go b/internal/db/application.go index 1011698bf..9f0109d59 100644 --- a/internal/db/application.go +++ b/internal/db/application.go @@ -36,15 +36,6 @@ type Application interface { // DeleteApplicationByClientID deletes the application with corresponding client_id value from the database. DeleteApplicationByClientID(ctx context.Context, clientID string) error - // GetClientByID fetches the application client from database with ID. - GetClientByID(ctx context.Context, id string) (*gtsmodel.Client, error) - - // PutClient puts the given application client in the database. - PutClient(ctx context.Context, client *gtsmodel.Client) error - - // DeleteClientByID deletes the application client from database with ID. - DeleteClientByID(ctx context.Context, id string) error - // GetAllTokens fetches all client oauth tokens from database. GetAllTokens(ctx context.Context) ([]*gtsmodel.Token, error) @@ -63,6 +54,9 @@ type Application interface { // PutToken puts given client oauth token in the database. PutToken(ctx context.Context, token *gtsmodel.Token) error + // UpdateToken updates the given token. Update all columns if no specific columns given. + UpdateToken(ctx context.Context, token *gtsmodel.Token, columns ...string) error + // DeleteTokenByID deletes client oauth token from database with ID. DeleteTokenByID(ctx context.Context, id string) error diff --git a/internal/db/bundb/admin.go b/internal/db/bundb/admin.go index ff398fca5..a311d2fc5 100644 --- a/internal/db/bundb/admin.go +++ b/internal/db/bundb/admin.go @@ -341,6 +341,7 @@ func (a *adminDB) CreateInstanceApplication(ctx context.Context) error { // instance account's ID so this is an easy check. instanceAcct, err := a.state.DB.GetInstanceAccount(ctx, "") if err != nil { + err := gtserror.Newf("db error getting instance account: %w", err) return err } @@ -369,18 +370,14 @@ func (a *adminDB) CreateInstanceApplication(ctx context.Context) error { clientID := instanceAcct.ID clientSecret := uuid.NewString() - appID, err := id.NewRandomULID() - if err != nil { - return err - } // Generate the application // to put in the database. app := >smodel.Application{ - ID: appID, + ID: id.NewULID(), Name: host + " instance application", Website: url, - RedirectURI: url, + RedirectURIs: []string{url}, ClientID: clientID, ClientSecret: clientSecret, Scopes: "write:accounts", @@ -388,19 +385,11 @@ func (a *adminDB) CreateInstanceApplication(ctx context.Context) error { // Store it. if err := a.state.DB.PutApplication(ctx, app); err != nil { + err := gtserror.Newf("db error storing instance application: %w", err) return err } - // Model an oauth client - // from the application. - oc := >smodel.Client{ - ID: clientID, - Secret: clientSecret, - Domain: url, - } - - // Store it. - return a.state.DB.PutClient(ctx, oc) + return nil } func (a *adminDB) GetInstanceApplication(ctx context.Context) (*gtsmodel.Application, error) { diff --git a/internal/db/bundb/application.go b/internal/db/bundb/application.go index 92fc5ea2b..e266a8ec6 100644 --- a/internal/db/bundb/application.go +++ b/internal/db/bundb/application.go @@ -97,41 +97,6 @@ func (a *applicationDB) DeleteApplicationByClientID(ctx context.Context, clientI return nil } -func (a *applicationDB) GetClientByID(ctx context.Context, id string) (*gtsmodel.Client, error) { - return a.state.Caches.DB.Client.LoadOne("ID", func() (*gtsmodel.Client, error) { - var client gtsmodel.Client - - if err := a.db.NewSelect(). - Model(&client). - Where("? = ?", bun.Ident("id"), id). - Scan(ctx); err != nil { - return nil, err - } - - return &client, nil - }, id) -} - -func (a *applicationDB) PutClient(ctx context.Context, client *gtsmodel.Client) error { - return a.state.Caches.DB.Client.Store(client, func() error { - _, err := a.db.NewInsert().Model(client).Exec(ctx) - return err - }) -} - -func (a *applicationDB) DeleteClientByID(ctx context.Context, id string) error { - _, err := a.db.NewDelete(). - Table("clients"). - Where("? = ?", bun.Ident("id"), id). - Exec(ctx) - if err != nil { - return err - } - - a.state.Caches.DB.Client.Invalidate("ID", id) - return nil -} - func (a *applicationDB) GetAllTokens(ctx context.Context) ([]*gtsmodel.Token, error) { var tokenIDs []string @@ -233,6 +198,21 @@ func (a *applicationDB) PutToken(ctx context.Context, token *gtsmodel.Token) err }) } +func (a *applicationDB) UpdateToken(ctx context.Context, token *gtsmodel.Token, columns ...string) error { + _, err := a.db. + NewUpdate(). + Model(token). + Column(columns...). + Where("? = ?", bun.Ident("id"), token.ID). + Exec(ctx) + if err != nil { + return err + } + + a.state.Caches.DB.Token.Invalidate("ID", token.ID) + return nil +} + func (a *applicationDB) DeleteTokenByID(ctx context.Context, id string) error { _, err := a.db.NewDelete(). Table("tokens"). diff --git a/internal/db/bundb/application_test.go b/internal/db/bundb/application_test.go index d03079f2a..b6b19319c 100644 --- a/internal/db/bundb/application_test.go +++ b/internal/db/bundb/application_test.go @@ -22,7 +22,6 @@ "errors" "reflect" "testing" - "time" "github.com/stretchr/testify/suite" "github.com/superseriousbusiness/gotosocial/internal/db" @@ -45,12 +44,6 @@ func (suite *ApplicationTestSuite) TestGetApplicationBy() { // isEqual checks if 2 application models are equal. isEqual := func(a1, a2 gtsmodel.Application) bool { - // Clear database-set fields. - a1.CreatedAt = time.Time{} - a2.CreatedAt = time.Time{} - a1.UpdatedAt = time.Time{} - a2.UpdatedAt = time.Time{} - return reflect.DeepEqual(a1, a2) } diff --git a/internal/db/bundb/bundb_test.go b/internal/db/bundb/bundb_test.go index 2fcf61aed..c128eca27 100644 --- a/internal/db/bundb/bundb_test.go +++ b/internal/db/bundb/bundb_test.go @@ -35,7 +35,6 @@ type BunDBStandardTestSuite struct { // standard suite models testTokens map[string]*gtsmodel.Token - testClients map[string]*gtsmodel.Client testApplications map[string]*gtsmodel.Application testUsers map[string]*gtsmodel.User testAccounts map[string]*gtsmodel.Account @@ -62,7 +61,6 @@ type BunDBStandardTestSuite struct { func (suite *BunDBStandardTestSuite) SetupSuite() { suite.testTokens = testrig.NewTestTokens() - suite.testClients = testrig.NewTestClients() suite.testApplications = testrig.NewTestApplications() suite.testUsers = testrig.NewTestUsers() suite.testAccounts = testrig.NewTestAccounts() diff --git a/internal/db/bundb/migrations/20250224105654_token_app_client_refactor.go b/internal/db/bundb/migrations/20250224105654_token_app_client_refactor.go new file mode 100644 index 000000000..2d25c649e --- /dev/null +++ b/internal/db/bundb/migrations/20250224105654_token_app_client_refactor.go @@ -0,0 +1,200 @@ +// GoToSocial +// Copyright (C) GoToSocial Authors admin@gotosocial.org +// SPDX-License-Identifier: AGPL-3.0-or-later +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package migrations + +import ( + "context" + + oldmodel "github.com/superseriousbusiness/gotosocial/internal/db/bundb/migrations/20211113114307_init" + newmodel "github.com/superseriousbusiness/gotosocial/internal/db/bundb/migrations/20250224105654_token_app_client_refactor" + "github.com/superseriousbusiness/gotosocial/internal/id" + "github.com/uptrace/bun" +) + +func init() { + up := func(ctx context.Context, db *bun.DB) error { + return db.RunInTx(ctx, nil, func(ctx context.Context, tx bun.Tx) error { + + // Drop unused clients table. + if _, err := tx. + NewDropTable(). + Table("clients"). + IfExists(). + Exec(ctx); err != nil { + return err + } + + // Select all old model + // applications into memory. + oldApps := []*oldmodel.Application{} + if err := tx. + NewSelect(). + Model(&oldApps). + Scan(ctx); err != nil { + return err + } + + // Drop the old applications table. + if _, err := tx. + NewDropTable(). + Table("applications"). + IfExists(). + Exec(ctx); err != nil { + return err + } + + // Create the new applications table. + if _, err := tx. + NewCreateTable(). + Model((*newmodel.Application)(nil)). + IfNotExists(). + Exec(ctx); err != nil { + return err + } + + // Add indexes to new applications table. + if _, err := tx. + NewCreateIndex(). + Table("applications"). + Index("applications_client_id_idx"). + Column("client_id"). + IfNotExists(). + Exec(ctx); err != nil { + return err + } + + if _, err := tx. + NewCreateIndex(). + Table("applications"). + Index("applications_managed_by_user_id_idx"). + Column("managed_by_user_id"). + IfNotExists(). + Exec(ctx); err != nil { + return err + } + + if len(oldApps) != 0 { + // Convert all the old model applications into new ones. + newApps := make([]*newmodel.Application, 0, len(oldApps)) + for _, oldApp := range oldApps { + newApps = append(newApps, &newmodel.Application{ + ID: id.NewULIDFromTime(oldApp.CreatedAt), + Name: oldApp.Name, + Website: oldApp.Website, + RedirectURIs: []string{oldApp.RedirectURI}, + ClientID: oldApp.ClientID, + ClientSecret: oldApp.ClientSecret, + Scopes: oldApp.Scopes, + }) + } + + // Whack all the new apps in + // there. Lads lads lads lads! + if _, err := tx. + NewInsert(). + Model(&newApps). + Exec(ctx); err != nil { + return err + } + } + + // Select all the old model + // tokens into memory. + oldTokens := []*oldmodel.Token{} + if err := tx. + NewSelect(). + Model(&oldTokens). + Scan(ctx); err != nil { + return err + } + + // Drop the old token table. + if _, err := tx. + NewDropTable(). + Table("tokens"). + IfExists(). + Exec(ctx); err != nil { + return err + } + + // Create the new token table. + if _, err := tx. + NewCreateTable(). + Model((*newmodel.Token)(nil)). + IfNotExists(). + Exec(ctx); err != nil { + return err + } + + // Add access index to new token table. + if _, err := tx. + NewCreateIndex(). + Table("tokens"). + Index("tokens_access_idx"). + Column("access"). + IfNotExists(). + Exec(ctx); err != nil { + return err + } + + if len(oldTokens) != 0 { + // Convert all the old model tokens into new ones. + newTokens := make([]*newmodel.Token, 0, len(oldTokens)) + for _, oldToken := range oldTokens { + newTokens = append(newTokens, &newmodel.Token{ + ID: id.NewULIDFromTime(oldToken.CreatedAt), + ClientID: oldToken.ClientID, + UserID: oldToken.UserID, + RedirectURI: oldToken.RedirectURI, + Scope: oldToken.Scope, + Code: oldToken.Code, + CodeChallenge: oldToken.CodeChallenge, + CodeChallengeMethod: oldToken.CodeChallengeMethod, + CodeCreateAt: oldToken.CodeCreateAt, + CodeExpiresAt: oldToken.CodeExpiresAt, + Access: oldToken.Access, + AccessCreateAt: oldToken.AccessCreateAt, + AccessExpiresAt: oldToken.AccessExpiresAt, + Refresh: oldToken.Refresh, + RefreshCreateAt: oldToken.RefreshCreateAt, + RefreshExpiresAt: oldToken.RefreshExpiresAt, + }) + } + + // Whack all the new tokens in + // there. Lads lads lads lads! + if _, err := tx. + NewInsert(). + Model(&newTokens). + Exec(ctx); err != nil { + return err + } + } + + return nil + }) + } + + down := func(ctx context.Context, db *bun.DB) error { + return nil + } + + if err := Migrations.Register(up, down); err != nil { + panic(err) + } +} diff --git a/internal/oauth/oauth_test.go b/internal/db/bundb/migrations/20250224105654_token_app_client_refactor/application.go similarity index 62% rename from internal/oauth/oauth_test.go rename to internal/db/bundb/migrations/20250224105654_token_app_client_refactor/application.go index 2b76024f7..efe2776ea 100644 --- a/internal/oauth/oauth_test.go +++ b/internal/db/bundb/migrations/20250224105654_token_app_client_refactor/application.go @@ -15,6 +15,15 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . -package oauth_test +package gtsmodel -// TODO: write tests +type Application struct { + ID string `bun:"type:CHAR(26),pk,nullzero,notnull,unique"` + Name string `bun:",notnull"` + Website string `bun:",nullzero"` + RedirectURIs []string `bun:"redirect_uris,array"` + ClientID string `bun:"type:CHAR(26),nullzero,notnull"` + ClientSecret string `bun:",nullzero,notnull"` + Scopes string `bun:",notnull"` + ManagedByUserID string `bun:"type:CHAR(26),nullzero"` +} diff --git a/internal/db/bundb/migrations/20250224105654_token_app_client_refactor/token.go b/internal/db/bundb/migrations/20250224105654_token_app_client_refactor/token.go new file mode 100644 index 000000000..46d30ba7d --- /dev/null +++ b/internal/db/bundb/migrations/20250224105654_token_app_client_refactor/token.go @@ -0,0 +1,42 @@ +// GoToSocial +// Copyright (C) GoToSocial Authors admin@gotosocial.org +// SPDX-License-Identifier: AGPL-3.0-or-later +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package gtsmodel + +import "time" + +// Token is a translation of the gotosocial token +// with the ExpiresIn fields replaced with ExpiresAt. +type Token struct { + ID string `bun:"type:CHAR(26),pk,nullzero,notnull,unique"` // id of this item in the database + LastUsed time.Time `bun:"type:timestamptz,nullzero"` // approximate time when this token was last used + ClientID string `bun:"type:CHAR(26),nullzero,notnull"` // ID of the client who owns this token + UserID string `bun:"type:CHAR(26),nullzero"` // ID of the user who owns this token + RedirectURI string `bun:",nullzero,notnull"` // Oauth redirect URI for this token + Scope string `bun:",nullzero,notnull,default:'read'"` // Oauth scope + Code string `bun:",pk,nullzero,notnull,default:''"` // Code, if present + CodeChallenge string `bun:",nullzero"` // Code challenge, if code present + CodeChallengeMethod string `bun:",nullzero"` // Code challenge method, if code present + CodeCreateAt time.Time `bun:"type:timestamptz,nullzero"` // Code created time, if code present + CodeExpiresAt time.Time `bun:"type:timestamptz,nullzero"` // Code expires at -- null means the code never expires + Access string `bun:",pk,nullzero,notnull,default:''"` // User level access token, if present + AccessCreateAt time.Time `bun:"type:timestamptz,nullzero"` // User level access token created time, if access present + AccessExpiresAt time.Time `bun:"type:timestamptz,nullzero"` // User level access token expires at -- null means the token never expires + Refresh string `bun:",pk,nullzero,notnull,default:''"` // Refresh token, if present + RefreshCreateAt time.Time `bun:"type:timestamptz,nullzero"` // Refresh created at, if refresh present + RefreshExpiresAt time.Time `bun:"type:timestamptz,nullzero"` // Refresh expires at -- null means the refresh token never expires +} diff --git a/internal/federation/federatingdb/federatingdb_test.go b/internal/federation/federatingdb/federatingdb_test.go index 2f07914ae..ee8f84e55 100644 --- a/internal/federation/federatingdb/federatingdb_test.go +++ b/internal/federation/federatingdb/federatingdb_test.go @@ -42,7 +42,6 @@ type FederatingDBTestSuite struct { state state.State testTokens map[string]*gtsmodel.Token - testClients map[string]*gtsmodel.Client testApplications map[string]*gtsmodel.Application testUsers map[string]*gtsmodel.User testAccounts map[string]*gtsmodel.Account @@ -61,7 +60,6 @@ func (suite *FederatingDBTestSuite) getFederatorMsg(timeout time.Duration) (*mes func (suite *FederatingDBTestSuite) SetupSuite() { suite.testTokens = testrig.NewTestTokens() - suite.testClients = testrig.NewTestClients() suite.testApplications = testrig.NewTestApplications() suite.testUsers = testrig.NewTestUsers() suite.testAccounts = testrig.NewTestAccounts() diff --git a/internal/filter/visibility/filter_test.go b/internal/filter/visibility/filter_test.go index 2d83ea759..ccd60f173 100644 --- a/internal/filter/visibility/filter_test.go +++ b/internal/filter/visibility/filter_test.go @@ -34,7 +34,6 @@ type FilterStandardTestSuite struct { // standard suite models testTokens map[string]*gtsmodel.Token - testClients map[string]*gtsmodel.Client testApplications map[string]*gtsmodel.Application testUsers map[string]*gtsmodel.User testAccounts map[string]*gtsmodel.Account @@ -49,7 +48,6 @@ type FilterStandardTestSuite struct { func (suite *FilterStandardTestSuite) SetupSuite() { suite.testTokens = testrig.NewTestTokens() - suite.testClients = testrig.NewTestClients() suite.testApplications = testrig.NewTestApplications() suite.testUsers = testrig.NewTestUsers() suite.testAccounts = testrig.NewTestAccounts() diff --git a/internal/gtsmodel/application.go b/internal/gtsmodel/application.go index 5f2d4f4b1..e8ef3bcf7 100644 --- a/internal/gtsmodel/application.go +++ b/internal/gtsmodel/application.go @@ -17,18 +17,39 @@ package gtsmodel -import "time" +import "strings" -// Application represents an application that can perform actions on behalf of a user. -// It is used to authorize tokens etc, and is associated with an oauth client id in the database. +// Application represents an application that +// can perform actions on behalf of a user. +// +// It is equivalent to an OAuth client. type Application struct { - ID string `bun:"type:CHAR(26),pk,nullzero,notnull,unique"` // id of this item in the database - CreatedAt time.Time `bun:"type:timestamptz,nullzero,notnull,default:current_timestamp"` // when was item created - UpdatedAt time.Time `bun:"type:timestamptz,nullzero,notnull,default:current_timestamp"` // when was item last updated - Name string `bun:",notnull"` // name of the application given when it was created (eg., 'tusky') - Website string `bun:",nullzero"` // website for the application given when it was created (eg., 'https://tusky.app') - RedirectURI string `bun:",nullzero,notnull"` // redirect uri requested by the application for oauth2 flow - ClientID string `bun:"type:CHAR(26),nullzero,notnull"` // id of the associated oauth client entity in the db - ClientSecret string `bun:",nullzero,notnull"` // secret of the associated oauth client entity in the db - Scopes string `bun:",notnull"` // scopes requested when this app was created + ID string `bun:"type:CHAR(26),pk,nullzero,notnull,unique"` // id of this item in the database + Name string `bun:",notnull"` // name of the application given when it was created (eg., 'tusky') + Website string `bun:",nullzero"` // website for the application given when it was created (eg., 'https://tusky.app') + RedirectURIs []string `bun:"redirect_uris,array"` // redirect uris requested by the application for oauth2 flow + ClientID string `bun:"type:CHAR(26),nullzero,notnull"` // id of the associated oauth client entity in the db + ClientSecret string `bun:",nullzero,notnull"` // secret of the associated oauth client entity in the db + Scopes string `bun:",notnull"` // scopes requested when this app was created + ManagedByUserID string `bun:"type:CHAR(26),nullzero"` // id of the user that manages this application, if it was created through the settings panel +} + +// Implements oauth2.ClientInfo. +func (a *Application) GetID() string { + return a.ClientID +} + +// Implements oauth2.ClientInfo. +func (a *Application) GetSecret() string { + return a.ClientSecret +} + +// Implements oauth2.ClientInfo. +func (a *Application) GetDomain() string { + return strings.Join(a.RedirectURIs, "\n") +} + +// Implements oauth2.ClientInfo. +func (a *Application) GetUserID() string { + return a.ManagedByUserID } diff --git a/internal/gtsmodel/client.go b/internal/gtsmodel/client.go deleted file mode 100644 index 35a85fdbe..000000000 --- a/internal/gtsmodel/client.go +++ /dev/null @@ -1,30 +0,0 @@ -// GoToSocial -// Copyright (C) GoToSocial Authors admin@gotosocial.org -// SPDX-License-Identifier: AGPL-3.0-or-later -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see . - -package gtsmodel - -import "time" - -// Client is a wrapper for OAuth client details. -type Client struct { - ID string `bun:"type:CHAR(26),pk,nullzero,notnull,unique"` // id of this item in the database - CreatedAt time.Time `bun:"type:timestamptz,nullzero,notnull,default:current_timestamp"` // when was item created - UpdatedAt time.Time `bun:"type:timestamptz,nullzero,notnull,default:current_timestamp"` // when was item last updated - Secret string `bun:",nullzero,notnull"` // secret generated when client was created - Domain string `bun:",nullzero,notnull"` // domain requested for client - UserID string `bun:"type:CHAR(26),nullzero"` // id of the user that this client acts on behalf of -} diff --git a/internal/gtsmodel/token.go b/internal/gtsmodel/token.go index 0586ae68a..6fe944290 100644 --- a/internal/gtsmodel/token.go +++ b/internal/gtsmodel/token.go @@ -22,22 +22,21 @@ // Token is a translation of the gotosocial token // with the ExpiresIn fields replaced with ExpiresAt. type Token struct { - ID string `bun:"type:CHAR(26),pk,nullzero,notnull,unique"` // id of this item in the database - CreatedAt time.Time `bun:"type:timestamptz,nullzero,notnull,default:current_timestamp"` // when was item created - UpdatedAt time.Time `bun:"type:timestamptz,nullzero,notnull,default:current_timestamp"` // when was item last updated - ClientID string `bun:"type:CHAR(26),nullzero,notnull"` // ID of the client who owns this token - UserID string `bun:"type:CHAR(26),nullzero"` // ID of the user who owns this token - RedirectURI string `bun:",nullzero,notnull"` // Oauth redirect URI for this token - Scope string `bun:",notnull"` // Oauth scope - Code string `bun:",pk,nullzero,notnull,default:''"` // Code, if present - CodeChallenge string `bun:",nullzero"` // Code challenge, if code present - CodeChallengeMethod string `bun:",nullzero"` // Code challenge method, if code present - CodeCreateAt time.Time `bun:"type:timestamptz,nullzero"` // Code created time, if code present - CodeExpiresAt time.Time `bun:"type:timestamptz,nullzero"` // Code expires at -- null means the code never expires - Access string `bun:",pk,nullzero,notnull,default:''"` // User level access token, if present - AccessCreateAt time.Time `bun:"type:timestamptz,nullzero"` // User level access token created time, if access present - AccessExpiresAt time.Time `bun:"type:timestamptz,nullzero"` // User level access token expires at -- null means the token never expires - Refresh string `bun:",pk,nullzero,notnull,default:''"` // Refresh token, if present - RefreshCreateAt time.Time `bun:"type:timestamptz,nullzero"` // Refresh created at, if refresh present - RefreshExpiresAt time.Time `bun:"type:timestamptz,nullzero"` // Refresh expires at -- null means the refresh token never expires + ID string `bun:"type:CHAR(26),pk,nullzero,notnull,unique"` // id of this item in the database + LastUsed time.Time `bun:"type:timestamptz,nullzero"` // approximate time when this token was last used + ClientID string `bun:"type:CHAR(26),nullzero,notnull"` // ID of the client who owns this token + UserID string `bun:"type:CHAR(26),nullzero"` // ID of the user who owns this token + RedirectURI string `bun:",nullzero,notnull"` // Oauth redirect URI for this token + Scope string `bun:",nullzero,notnull,default:'read'"` // Oauth scope // Oauth scope + Code string `bun:",pk,nullzero,notnull,default:''"` // Code, if present + CodeChallenge string `bun:",nullzero"` // Code challenge, if code present + CodeChallengeMethod string `bun:",nullzero"` // Code challenge method, if code present + CodeCreateAt time.Time `bun:"type:timestamptz,nullzero"` // Code created time, if code present + CodeExpiresAt time.Time `bun:"type:timestamptz,nullzero"` // Code expires at -- null means the code never expires + Access string `bun:",pk,nullzero,notnull,default:''"` // User level access token, if present + AccessCreateAt time.Time `bun:"type:timestamptz,nullzero"` // User level access token created time, if access present + AccessExpiresAt time.Time `bun:"type:timestamptz,nullzero"` // User level access token expires at -- null means the token never expires + Refresh string `bun:",pk,nullzero,notnull,default:''"` // Refresh token, if present + RefreshCreateAt time.Time `bun:"type:timestamptz,nullzero"` // Refresh created at, if refresh present + RefreshExpiresAt time.Time `bun:"type:timestamptz,nullzero"` // Refresh expires at -- null means the refresh token never expires } diff --git a/internal/oauth/clientstore.go b/internal/oauth/clientstore.go index af48edac3..1191d798d 100644 --- a/internal/oauth/clientstore.go +++ b/internal/oauth/clientstore.go @@ -21,45 +21,29 @@ "context" "codeberg.org/superseriousbusiness/oauth2/v4" - "codeberg.org/superseriousbusiness/oauth2/v4/models" - "github.com/superseriousbusiness/gotosocial/internal/db" - "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" + "github.com/superseriousbusiness/gotosocial/internal/state" ) type clientStore struct { - db db.DB + state *state.State } -// NewClientStore returns an implementation of the oauth2 ClientStore interface, using the given db as a storage backend. -func NewClientStore(db db.DB) oauth2.ClientStore { - pts := &clientStore{ - db: db, - } - return pts +// NewClientStore returns a minimal implementation of +// oauth2.ClientStore interface, using state as storage. +// +// Only GetByID is implemented, Set and Delete are stubs. +func NewClientStore(state *state.State) oauth2.ClientStore { + return &clientStore{state: state} } func (cs *clientStore) GetByID(ctx context.Context, clientID string) (oauth2.ClientInfo, error) { - client, err := cs.db.GetClientByID(ctx, clientID) - if err != nil { - return nil, err - } - return models.New( - client.ID, - client.Secret, - client.Domain, - client.UserID, - ), nil + return cs.state.DB.GetApplicationByClientID(ctx, clientID) } -func (cs *clientStore) Set(ctx context.Context, id string, cli oauth2.ClientInfo) error { - return cs.db.PutClient(ctx, >smodel.Client{ - ID: cli.GetID(), - Secret: cli.GetSecret(), - Domain: cli.GetDomain(), - UserID: cli.GetUserID(), - }) +func (cs *clientStore) Set(_ context.Context, _ string, _ oauth2.ClientInfo) error { + return nil } -func (cs *clientStore) Delete(ctx context.Context, id string) error { - return cs.db.DeleteClientByID(ctx, id) +func (cs *clientStore) Delete(_ context.Context, _ string) error { + return nil } diff --git a/internal/oauth/clientstore_test.go b/internal/oauth/clientstore_test.go index 59b0ec1d3..3548d1af7 100644 --- a/internal/oauth/clientstore_test.go +++ b/internal/oauth/clientstore_test.go @@ -25,89 +25,55 @@ "github.com/stretchr/testify/suite" "github.com/superseriousbusiness/gotosocial/internal/admin" "github.com/superseriousbusiness/gotosocial/internal/db" + "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" "github.com/superseriousbusiness/gotosocial/internal/oauth" "github.com/superseriousbusiness/gotosocial/internal/state" "github.com/superseriousbusiness/gotosocial/testrig" ) -type PgClientStoreTestSuite struct { +type ClientStoreTestSuite struct { suite.Suite db db.DB state state.State - testClientID string - testClientSecret string - testClientDomain string - testClientUserID string + testApplications map[string]*gtsmodel.Application } -// SetupSuite sets some variables on the suite that we can use as consts (more or less) throughout -func (suite *PgClientStoreTestSuite) SetupSuite() { - suite.testClientID = "01FCVB74EW6YBYAEY7QG9CQQF6" - suite.testClientSecret = "4cc87402-259b-4a35-9485-2c8bf54f3763" - suite.testClientDomain = "https://example.org" - suite.testClientUserID = "01FEGYXKVCDB731QF9MVFXA4F5" +func (suite *ClientStoreTestSuite) SetupSuite() { + suite.testApplications = testrig.NewTestApplications() } -// SetupTest creates a postgres connection and creates the oauth_clients table before each test -func (suite *PgClientStoreTestSuite) SetupTest() { +func (suite *ClientStoreTestSuite) SetupTest() { suite.state.Caches.Init() - testrig.InitTestLog() testrig.InitTestConfig() + testrig.InitTestLog() suite.db = testrig.NewTestDB(&suite.state) suite.state.DB = suite.db suite.state.AdminActions = admin.New(suite.state.DB, &suite.state.Workers) testrig.StandardDBSetup(suite.db, nil) } -// TearDownTest drops the oauth_clients table and closes the pg connection after each test -func (suite *PgClientStoreTestSuite) TearDownTest() { +func (suite *ClientStoreTestSuite) TearDownTest() { testrig.StandardDBTeardown(suite.db) } -func (suite *PgClientStoreTestSuite) TestClientStoreSetAndGet() { - // set a new client in the store - cs := oauth.NewClientStore(suite.db) - if err := cs.Set(context.Background(), suite.testClientID, models.New(suite.testClientID, suite.testClientSecret, suite.testClientDomain, suite.testClientUserID)); err != nil { - suite.FailNow(err.Error()) - } +func (suite *ClientStoreTestSuite) TestClientStoreGet() { + testApp := suite.testApplications["application_1"] + cs := oauth.NewClientStore(&suite.state) - // fetch that client from the store - client, err := cs.GetByID(context.Background(), suite.testClientID) + // Fetch clientInfo from the store. + clientInfo, err := cs.GetByID(context.Background(), testApp.ClientID) if err != nil { suite.FailNow(err.Error()) } - // check that the values are the same - suite.NotNil(client) - suite.EqualValues(models.New(suite.testClientID, suite.testClientSecret, suite.testClientDomain, suite.testClientUserID), client) + // Check expected values. + suite.NotNil(clientInfo) + suite.Equal(testApp.ClientID, clientInfo.GetID()) + suite.Equal(testApp.ClientSecret, clientInfo.GetSecret()) + suite.Equal(testApp.RedirectURIs[0], clientInfo.GetDomain()) + suite.Equal(testApp.ManagedByUserID, clientInfo.GetUserID()) } -func (suite *PgClientStoreTestSuite) TestClientSetAndDelete() { - // set a new client in the store - cs := oauth.NewClientStore(suite.db) - if err := cs.Set(context.Background(), suite.testClientID, models.New(suite.testClientID, suite.testClientSecret, suite.testClientDomain, suite.testClientUserID)); err != nil { - suite.FailNow(err.Error()) - } - - // fetch the client from the store - client, err := cs.GetByID(context.Background(), suite.testClientID) - if err != nil { - suite.FailNow(err.Error()) - } - - // check that the values are the same - suite.NotNil(client) - suite.EqualValues(models.New(suite.testClientID, suite.testClientSecret, suite.testClientDomain, suite.testClientUserID), client) - if err := cs.Delete(context.Background(), suite.testClientID); err != nil { - suite.FailNow(err.Error()) - } - - // try to get the deleted client; we should get an error - deletedClient, err := cs.GetByID(context.Background(), suite.testClientID) - suite.Assert().Nil(deletedClient) - suite.Assert().EqualValues(db.ErrNoEntries, err) -} - -func TestPgClientStoreTestSuite(t *testing.T) { - suite.Run(t, new(PgClientStoreTestSuite)) +func TestClientStoreTestSuite(t *testing.T) { + suite.Run(t, new(ClientStoreTestSuite)) } diff --git a/internal/oauth/server.go b/internal/oauth/server.go index 8330ee179..8475555ef 100644 --- a/internal/oauth/server.go +++ b/internal/oauth/server.go @@ -22,6 +22,8 @@ "errors" "fmt" "net/http" + "net/url" + "slices" "strings" "codeberg.org/superseriousbusiness/oauth2/v4" @@ -30,7 +32,10 @@ "codeberg.org/superseriousbusiness/oauth2/v4/server" "github.com/superseriousbusiness/gotosocial/internal/db" "github.com/superseriousbusiness/gotosocial/internal/gtserror" + "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" "github.com/superseriousbusiness/gotosocial/internal/log" + "github.com/superseriousbusiness/gotosocial/internal/state" + "github.com/superseriousbusiness/gotosocial/internal/util" ) const ( @@ -75,17 +80,58 @@ type s struct { } // New returns a new oauth server that implements the Server interface -func New(ctx context.Context, database db.DB) Server { - ts := newTokenStore(ctx, database) - cs := NewClientStore(database) +func New( + ctx context.Context, + state *state.State, + clientScopeHandler server.ClientScopeHandler, +) Server { + ts := newTokenStore(ctx, state) + cs := NewClientStore(state) manager := manage.NewDefaultManager() manager.MapTokenStorage(ts) manager.MapClientStorage(cs) manager.SetAuthorizeCodeTokenCfg(&manage.Config{ - AccessTokenExp: 0, // access tokens don't expire -- they must be revoked - IsGenerateRefresh: false, // don't use refresh tokens + // Following the Mastodon API, + // access tokens don't expire. + AccessTokenExp: 0, + // Don't use refresh tokens. + IsGenerateRefresh: false, }) + + manager.SetValidateURIHandler(func(hasRedirectList, wantsRedirect string) error { + wantsRedirectURI, err := url.Parse(wantsRedirect) + if err != nil { + return err + } + + // Redirect URIs are given to us as + // a list of URIs, newline-separated. + // + // Ensure that one of them matches + // requested redirectURI. + hasRedirects := strings.Split(hasRedirectList, "\n") + + if slices.ContainsFunc( + hasRedirects, + func(hasRedirect string) bool { + hasRedirectURI, err := url.Parse(hasRedirect) + if err != nil { + log.Errorf(nil, "error parsing hasRedirect: %v", err) + return false + } + + // Want an exact match. + // See: https://www.oauth.com/oauth2-servers/redirect-uris/redirect-uri-validation/ + return wantsRedirectURI.String() == hasRedirectURI.String() + }, + ) { + return nil + } + + return oautherr.ErrInvalidRedirectURI + }) + sc := &server.Config{ TokenType: "Bearer", // Must follow the spec. @@ -106,6 +152,19 @@ func New(ctx context.Context, database db.DB) Server { } srv := server.NewServer(sc, manager) + + srv.SetAuthorizeScopeHandler(func(w http.ResponseWriter, r *http.Request) (string, error) { + // Use provided scope or + // fall back to default "read". + scope := r.FormValue("scope") + if strings.TrimSpace(scope) == "" { + scope = "read" + } + return scope, nil + }) + + srv.SetClientScopeHandler(clientScopeHandler) + srv.SetInternalErrorHandler(func(err error) *oautherr.Response { log.Errorf(nil, "internal oauth error: %s", err) return nil @@ -122,10 +181,10 @@ func New(ctx context.Context, database db.DB) Server { } return userID, nil }) + srv.SetClientInfoHandler(server.ClientFormHandler) - return &s{ - server: srv, - } + + return &s{srv} } // HandleTokenRequest wraps the oauth2 library's HandleTokenRequest function @@ -143,31 +202,42 @@ func (s *s) HandleTokenRequest(r *http.Request) (map[string]interface{}, gtserro } ti, err := s.server.GetAccessToken(ctx, gt, tgr) - if err != nil { + switch { + case err == nil: + // No problem. + break + case errors.Is(err, oautherr.ErrInvalidScope): + help := fmt.Sprintf("requested scope %s was not covered by client scope", tgr.Scope) + return nil, gtserror.NewErrorForbidden(err, help, HelpfulAdvice) + case errors.Is(err, oautherr.ErrInvalidRedirectURI): + help := fmt.Sprintf("requested redirect URI %s was not covered by client redirect URIs", tgr.RedirectURI) + return nil, gtserror.NewErrorForbidden(err, help, HelpfulAdvice) + default: help := fmt.Sprintf("could not get access token: %s", err) return nil, gtserror.NewErrorBadRequest(err, help, HelpfulAdvice) } + // Wrangle data a bit. data := s.server.GetTokenData(ti) + // Add created_at for Mastodon API compatibility. + data["created_at"] = ti.GetAccessCreateAt().Unix() + + // If expires_in is 0 or less, omit it + // from serialization so that clients don't + // interpret the token as already expired. if expiresInI, ok := data["expires_in"]; ok { - switch expiresIn := expiresInI.(type) { - case int64: - // remove this key from the returned map - // if the value is 0 or less, so that clients - // don't interpret the token as already expired - if expiresIn <= 0 { - delete(data, "expires_in") - } - default: - err := errors.New("expires_in was set on token response, but was not an int64") - return nil, gtserror.NewErrorInternalError(err, HelpfulAdvice) + expiresIn, ok := expiresInI.(int64) + if !ok { + log.Panicf(ctx, "could not cast expires_in %T as int64", expiresInI) + return nil, nil + } + + if expiresIn <= 0 { + delete(data, "expires_in") } } - // add this for mastodon api compatibility - data["created_at"] = ti.GetAccessCreateAt().Unix() - return data, nil } @@ -207,7 +277,7 @@ func (s *s) HandleAuthorizeRequest(w http.ResponseWriter, r *http.Request) gtser } req.UserID = userID - // specify the scope of authorization + // Specify the scope of authorization. if fn := s.server.AuthorizeScopeHandler; fn != nil { scope, err := fn(w, r) if err != nil { @@ -217,7 +287,7 @@ func (s *s) HandleAuthorizeRequest(w http.ResponseWriter, r *http.Request) gtser } } - // specify the expiration time of access token + // Specify the expiration time of access token. if fn := s.server.AccessTokenExpHandler; fn != nil { exp, err := fn(w, r) if err != nil { @@ -231,13 +301,28 @@ func (s *s) HandleAuthorizeRequest(w http.ResponseWriter, r *http.Request) gtser return s.errorOrRedirect(err, w, req) } - // If the redirect URI is empty, the default domain provided by the client is used. + // If the redirect URI is empty, use the + // first of the client's redirect URIs. if req.RedirectURI == "" { client, err := s.server.Manager.GetClient(ctx, req.ClientID) - if err != nil { + if err != nil && !errors.Is(err, db.ErrNoEntries) { + // Real error. + err := gtserror.Newf("db error getting application with client id %s: %w", req.ClientID, err) + return gtserror.NewErrorInternalError(err) + } + + if util.IsNil(client) { + // Application just not found. return gtserror.NewErrorUnauthorized(err, HelpfulAdvice) } - req.RedirectURI = client.GetDomain() + + app, ok := client.(*gtsmodel.Application) + if !ok { + log.Panicf(ctx, "could not cast %T to *gtsmodel.Application", client) + return nil + } + + req.RedirectURI = app.RedirectURIs[0] } uri, err := s.server.GetRedirectURI(req, s.server.GetAuthorizeData(req.ResponseType, ti)) diff --git a/internal/oauth/tokenstore.go b/internal/oauth/tokenstore.go index df2e419fe..672ce7751 100644 --- a/internal/oauth/tokenstore.go +++ b/internal/oauth/tokenstore.go @@ -22,30 +22,32 @@ "errors" "time" + "codeberg.org/gruf/go-mutexes" "codeberg.org/superseriousbusiness/oauth2/v4" "codeberg.org/superseriousbusiness/oauth2/v4/models" - "github.com/superseriousbusiness/gotosocial/internal/db" + "github.com/superseriousbusiness/gotosocial/internal/gtserror" "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" "github.com/superseriousbusiness/gotosocial/internal/id" "github.com/superseriousbusiness/gotosocial/internal/log" + "github.com/superseriousbusiness/gotosocial/internal/state" ) // tokenStore is an implementation of oauth2.TokenStore, which uses our db interface as a storage backend. type tokenStore struct { oauth2.TokenStore - db db.DB + state *state.State + lastUsedLocks mutexes.MutexMap } // newTokenStore returns a token store that satisfies the oauth2.TokenStore interface. // // In order to allow tokens to 'expire', it will also set off a goroutine that iterates through // the tokens in the DB once per minute and deletes any that have expired. -func newTokenStore(ctx context.Context, db db.DB) oauth2.TokenStore { - ts := &tokenStore{ - db: db, - } +func newTokenStore(ctx context.Context, state *state.State) oauth2.TokenStore { + ts := &tokenStore{state: state} - // set the token store to clean out expired tokens once per minute, or return if we're done + // Set the token store to clean out expired tokens + // once per minute, or return if we're done. go func(ctx context.Context, ts *tokenStore) { cleanloop: for { @@ -64,25 +66,48 @@ func newTokenStore(ctx context.Context, db db.DB) oauth2.TokenStore { return ts } -// sweep clears out old tokens that have expired; it should be run on a loop about once per minute or so. +// sweep clears out old tokens that have expired; +// it should be run on a loop about once per minute or so. func (ts *tokenStore) sweep(ctx context.Context) error { - // select *all* tokens from the db - // todo: if this becomes expensive (ie., there are fucking LOADS of tokens) then figure out a better way. - tokens, err := ts.db.GetAllTokens(ctx) + // Select *all* tokens from the db + // + // TODO: if this becomes expensive + // (ie., there are fucking LOADS of + // tokens) then figure out a better way. + tokens, err := ts.state.DB.GetAllTokens(ctx) if err != nil { return err } - // iterate through and remove expired tokens + // Remove any expired tokens, bearing + // in mind that zero time = no expiry. now := time.Now() - for _, dbt := range tokens { - // The zero value of a time.Time is 00:00 january 1 1970, which will always be before now. So: - // we only want to check if a token expired before now if the expiry time is *not zero*; - // ie., if it's been explicity set. - if !dbt.CodeExpiresAt.IsZero() && dbt.CodeExpiresAt.Before(now) || !dbt.RefreshExpiresAt.IsZero() && dbt.RefreshExpiresAt.Before(now) || !dbt.AccessExpiresAt.IsZero() && dbt.AccessExpiresAt.Before(now) { - if err := ts.db.DeleteTokenByID(ctx, dbt.ID); err != nil { - return err - } + for _, token := range tokens { + var expired bool + + switch { + case !token.CodeExpiresAt.IsZero() && token.CodeExpiresAt.Before(now): + log.Tracef(ctx, "code token %s is expired", token.ID) + expired = true + + case !token.RefreshExpiresAt.IsZero() && token.RefreshExpiresAt.Before(now): + log.Tracef(ctx, "refresh token %s is expired", token.ID) + expired = true + + case !token.AccessExpiresAt.IsZero() && token.AccessExpiresAt.Before(now): + log.Tracef(ctx, "access token %s is expired", token.ID) + expired = true + } + + if !expired { + // Token's + // still good. + continue + } + + if err := ts.state.DB.DeleteTokenByID(ctx, token.ID); err != nil { + err := gtserror.Newf("db error expiring token %s: %w", token.ID, err) + return err } } @@ -90,7 +115,6 @@ func (ts *tokenStore) sweep(ctx context.Context) error { } // Create creates and store the new token information. -// For the original implementation, see https://codeberg.org/superseriousbusiness/oauth2/blob/master/store/token.go#L34 func (ts *tokenStore) Create(ctx context.Context, info oauth2.TokenInfo) error { t, ok := info.(*models.Token) if !ok { @@ -99,55 +123,97 @@ func (ts *tokenStore) Create(ctx context.Context, info oauth2.TokenInfo) error { dbt := TokenToDBToken(t) if dbt.ID == "" { - dbtID, err := id.NewRandomULID() - if err != nil { - return err - } - dbt.ID = dbtID + dbt.ID = id.NewULID() } - return ts.db.PutToken(ctx, dbt) + return ts.state.DB.PutToken(ctx, dbt) } // RemoveByCode deletes a token from the DB based on the Code field func (ts *tokenStore) RemoveByCode(ctx context.Context, code string) error { - return ts.db.DeleteTokenByCode(ctx, code) + return ts.state.DB.DeleteTokenByCode(ctx, code) } // RemoveByAccess deletes a token from the DB based on the Access field func (ts *tokenStore) RemoveByAccess(ctx context.Context, access string) error { - return ts.db.DeleteTokenByAccess(ctx, access) + return ts.state.DB.DeleteTokenByAccess(ctx, access) } // RemoveByRefresh deletes a token from the DB based on the Refresh field func (ts *tokenStore) RemoveByRefresh(ctx context.Context, refresh string) error { - return ts.db.DeleteTokenByRefresh(ctx, refresh) + return ts.state.DB.DeleteTokenByRefresh(ctx, refresh) } -// GetByCode selects a token from the DB based on the Code field -func (ts *tokenStore) GetByCode(ctx context.Context, code string) (oauth2.TokenInfo, error) { - token, err := ts.db.GetTokenByCode(ctx, code) - if err != nil { - return nil, err - } - return DBTokenToToken(token), nil +// GetByCode selects a token from +// the DB based on the Code field +func (ts *tokenStore) GetByCode( + ctx context.Context, + code string, +) (oauth2.TokenInfo, error) { + return ts.getUpdateToken( + ctx, + ts.state.DB.GetTokenByCode, + code, + ) } -// GetByAccess selects a token from the DB based on the Access field -func (ts *tokenStore) GetByAccess(ctx context.Context, access string) (oauth2.TokenInfo, error) { - token, err := ts.db.GetTokenByAccess(ctx, access) - if err != nil { - return nil, err - } - return DBTokenToToken(token), nil +// GetByAccess selects a token from +// the DB based on the Access field. +func (ts *tokenStore) GetByAccess( + ctx context.Context, + access string, +) (oauth2.TokenInfo, error) { + return ts.getUpdateToken( + ctx, + ts.state.DB.GetTokenByAccess, + access, + ) } -// GetByRefresh selects a token from the DB based on the Refresh field -func (ts *tokenStore) GetByRefresh(ctx context.Context, refresh string) (oauth2.TokenInfo, error) { - token, err := ts.db.GetTokenByRefresh(ctx, refresh) +// GetByRefresh selects a token from +// the DB based on the Refresh field +func (ts *tokenStore) GetByRefresh( + ctx context.Context, + refresh string, +) (oauth2.TokenInfo, error) { + return ts.getUpdateToken( + ctx, + ts.state.DB.GetTokenByRefresh, + refresh, + ) +} + +// package-internal function for getting a token +// and potentially updating its last_used value. +func (ts *tokenStore) getUpdateToken( + ctx context.Context, + getBy func(context.Context, string) (*gtsmodel.Token, error), + key string, +) (oauth2.TokenInfo, error) { + // Hold a lock to get the token based on + // whatever func + key we've been given. + unlock := ts.lastUsedLocks.Lock(key) + + token, err := getBy(ctx, key) if err != nil { + // Unlock on error. + unlock() return nil, err } + + // If token was last used more than + // an hour ago, update this in the db. + wasLastUsed := token.LastUsed + if time.Since(wasLastUsed) > 1*time.Hour { + token.LastUsed = time.Now() + if err := ts.state.DB.UpdateToken(ctx, token, "last_used"); err != nil { + err := gtserror.Newf("error updating last_used on token: %w", err) + return nil, err + } + } + + // We're done, unlock. + unlock() return DBTokenToToken(token), nil } diff --git a/internal/processing/account/account_test.go b/internal/processing/account/account_test.go index 7bd9658dc..4173162cc 100644 --- a/internal/processing/account/account_test.go +++ b/internal/processing/account/account_test.go @@ -55,7 +55,6 @@ type AccountStandardTestSuite struct { // standard suite models testTokens map[string]*gtsmodel.Token - testClients map[string]*gtsmodel.Client testApplications map[string]*gtsmodel.Application testUsers map[string]*gtsmodel.User testAccounts map[string]*gtsmodel.Account @@ -76,7 +75,6 @@ func (suite *AccountStandardTestSuite) getClientMsg(timeout time.Duration) (*mes func (suite *AccountStandardTestSuite) SetupSuite() { suite.testTokens = testrig.NewTestTokens() - suite.testClients = testrig.NewTestClients() suite.testApplications = testrig.NewTestApplications() suite.testUsers = testrig.NewTestUsers() suite.testAccounts = testrig.NewTestAccounts() diff --git a/internal/processing/account/delete.go b/internal/processing/account/delete.go index 2618fdfc5..0064d7eb4 100644 --- a/internal/processing/account/delete.go +++ b/internal/processing/account/delete.go @@ -113,11 +113,6 @@ func (p *Processor) deleteUserAndTokensForAccount(ctx context.Context, account * } for _, t := range tokens { - // Delete any OAuth clients associated with this token. - if err := p.state.DB.DeleteByID(ctx, t.ClientID, &[]*gtsmodel.Client{}); err != nil { - return gtserror.Newf("db error deleting client: %w", err) - } - // Delete any OAuth applications associated with this token. if err := p.state.DB.DeleteApplicationByClientID(ctx, t.ClientID); err != nil { return gtserror.Newf("db error deleting application: %w", err) diff --git a/internal/processing/admin/admin_test.go b/internal/processing/admin/admin_test.go index ad9d9b2ae..804abbc62 100644 --- a/internal/processing/admin/admin_test.go +++ b/internal/processing/admin/admin_test.go @@ -58,7 +58,6 @@ type AdminStandardTestSuite struct { // standard suite models testTokens map[string]*gtsmodel.Token - testClients map[string]*gtsmodel.Client testApplications map[string]*gtsmodel.Application testUsers map[string]*gtsmodel.User testAccounts map[string]*gtsmodel.Account @@ -73,7 +72,6 @@ type AdminStandardTestSuite struct { func (suite *AdminStandardTestSuite) SetupSuite() { suite.testTokens = testrig.NewTestTokens() - suite.testClients = testrig.NewTestClients() suite.testApplications = testrig.NewTestApplications() suite.testUsers = testrig.NewTestUsers() suite.testAccounts = testrig.NewTestAccounts() @@ -103,7 +101,7 @@ func (suite *AdminStandardTestSuite) SetupTest() { suite.storage = testrig.NewInMemoryStorage() suite.state.Storage = suite.storage suite.mediaManager = testrig.NewTestMediaManager(&suite.state) - suite.oauthServer = testrig.NewTestOauthServer(suite.db) + suite.oauthServer = testrig.NewTestOauthServer(&suite.state) suite.transportController = testrig.NewTestTransportController(&suite.state, testrig.NewMockHTTPClient(nil, "../../../testrig/media")) suite.federator = testrig.NewTestFederator(&suite.state, suite.transportController, suite.mediaManager) diff --git a/internal/processing/app.go b/internal/processing/app.go index 2a43c5212..c9bd4eb68 100644 --- a/internal/processing/app.go +++ b/internal/processing/app.go @@ -19,6 +19,9 @@ import ( "context" + "fmt" + "net/url" + "strings" "github.com/google/uuid" apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" @@ -26,10 +29,12 @@ "github.com/superseriousbusiness/gotosocial/internal/gtserror" "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" "github.com/superseriousbusiness/gotosocial/internal/id" + "github.com/superseriousbusiness/gotosocial/internal/oauth" ) func (p *Processor) AppCreate(ctx context.Context, authed *apiutil.Auth, form *apimodel.ApplicationCreateRequest) (*apimodel.Application, gtserror.WithCode) { - // set default 'read' for scopes if it's not set + // Set default 'read' for + // scopes if it's not set. var scopes string if form.Scopes == "" { scopes = "read" @@ -37,48 +42,47 @@ func (p *Processor) AppCreate(ctx context.Context, authed *apiutil.Auth, form *a scopes = form.Scopes } - // generate new IDs for this application and its associated client + // Normalize + parse requested redirect URIs. + form.RedirectURIs = strings.TrimSpace(form.RedirectURIs) + var redirectURIs []string + if form.RedirectURIs != "" { + // Redirect URIs can be just one value, or can be passed + // as a newline-separated list of strings. Ensure each URI + // is parseable + normalize it by reconstructing from *url.URL. + for _, redirectStr := range strings.Split(form.RedirectURIs, "\n") { + redirectURI, err := url.Parse(redirectStr) + if err != nil { + errText := fmt.Sprintf("error parsing redirect URI: %v", err) + return nil, gtserror.NewErrorBadRequest(err, errText) + } + redirectURIs = append(redirectURIs, redirectURI.String()) + } + } else { + // No redirect URI(s) provided, just set default oob. + redirectURIs = append(redirectURIs, oauth.OOBURI) + } + + // Generate random client ID. clientID, err := id.NewRandomULID() if err != nil { return nil, gtserror.NewErrorInternalError(err) } - clientSecret := uuid.NewString() - appID, err := id.NewRandomULID() - if err != nil { - return nil, gtserror.NewErrorInternalError(err) - } - - // generate the application to put in the database + // Generate + store app + // to put in the database. app := >smodel.Application{ - ID: appID, + ID: id.NewULID(), Name: form.ClientName, Website: form.Website, - RedirectURI: form.RedirectURIs, + RedirectURIs: redirectURIs, ClientID: clientID, - ClientSecret: clientSecret, + ClientSecret: uuid.NewString(), Scopes: scopes, } - - // chuck it in the db if err := p.state.DB.PutApplication(ctx, app); err != nil { return nil, gtserror.NewErrorInternalError(err) } - // now we need to model an oauth client from the application that the oauth library can use - oc := >smodel.Client{ - ID: clientID, - Secret: clientSecret, - Domain: form.RedirectURIs, - // This client isn't yet associated with a specific user, it's just an app client right now - UserID: "", - } - - // chuck it in the db - if err := p.state.DB.PutClient(ctx, oc); err != nil { - return nil, gtserror.NewErrorInternalError(err) - } - apiApp, err := p.converter.AppToAPIAppSensitive(ctx, app) if err != nil { return nil, gtserror.NewErrorInternalError(err) diff --git a/internal/processing/conversations/conversations_test.go b/internal/processing/conversations/conversations_test.go index 831ba1a43..fecaf5666 100644 --- a/internal/processing/conversations/conversations_test.go +++ b/internal/processing/conversations/conversations_test.go @@ -57,7 +57,6 @@ type ConversationsTestSuite struct { // standard suite models testTokens map[string]*gtsmodel.Token - testClients map[string]*gtsmodel.Client testApplications map[string]*gtsmodel.Application testUsers map[string]*gtsmodel.User testAccounts map[string]*gtsmodel.Account @@ -84,7 +83,6 @@ func (suite *ConversationsTestSuite) getClientMsg(timeout time.Duration) (*messa func (suite *ConversationsTestSuite) SetupSuite() { suite.testTokens = testrig.NewTestTokens() - suite.testClients = testrig.NewTestClients() suite.testApplications = testrig.NewTestApplications() suite.testUsers = testrig.NewTestUsers() suite.testAccounts = testrig.NewTestAccounts() diff --git a/internal/processing/media/media_test.go b/internal/processing/media/media_test.go index 2930733c4..6d44321b7 100644 --- a/internal/processing/media/media_test.go +++ b/internal/processing/media/media_test.go @@ -45,7 +45,6 @@ type MediaStandardTestSuite struct { // standard suite models testTokens map[string]*gtsmodel.Token - testClients map[string]*gtsmodel.Client testApplications map[string]*gtsmodel.Application testUsers map[string]*gtsmodel.User testAccounts map[string]*gtsmodel.Account @@ -59,7 +58,6 @@ type MediaStandardTestSuite struct { func (suite *MediaStandardTestSuite) SetupSuite() { suite.testTokens = testrig.NewTestTokens() - suite.testClients = testrig.NewTestClients() suite.testApplications = testrig.NewTestApplications() suite.testUsers = testrig.NewTestUsers() suite.testAccounts = testrig.NewTestAccounts() diff --git a/internal/processing/processor_test.go b/internal/processing/processor_test.go index 9cf6cbd60..4b6406b03 100644 --- a/internal/processing/processor_test.go +++ b/internal/processing/processor_test.go @@ -58,7 +58,6 @@ type ProcessingStandardTestSuite struct { // standard suite models testTokens map[string]*gtsmodel.Token - testClients map[string]*gtsmodel.Client testApplications map[string]*gtsmodel.Application testUsers map[string]*gtsmodel.User testAccounts map[string]*gtsmodel.Account @@ -77,7 +76,6 @@ type ProcessingStandardTestSuite struct { func (suite *ProcessingStandardTestSuite) SetupSuite() { suite.testTokens = testrig.NewTestTokens() - suite.testClients = testrig.NewTestClients() suite.testApplications = testrig.NewTestApplications() suite.testUsers = testrig.NewTestUsers() suite.testAccounts = testrig.NewTestAccounts() @@ -124,7 +122,7 @@ func (suite *ProcessingStandardTestSuite) SetupTest() { suite.transportController = testrig.NewTestTransportController(&suite.state, suite.httpClient) suite.mediaManager = testrig.NewTestMediaManager(&suite.state) suite.federator = testrig.NewTestFederator(&suite.state, suite.transportController, suite.mediaManager) - suite.oauthServer = testrig.NewTestOauthServer(suite.db) + suite.oauthServer = testrig.NewTestOauthServer(&suite.state) suite.emailSender = testrig.NewEmailSender("../../web/template/", nil) suite.processor = processing.NewProcessor( diff --git a/internal/processing/status/status_test.go b/internal/processing/status/status_test.go index 74aef7188..c163f95a7 100644 --- a/internal/processing/status/status_test.go +++ b/internal/processing/status/status_test.go @@ -50,7 +50,6 @@ type StatusStandardTestSuite struct { // standard suite models testTokens map[string]*gtsmodel.Token - testClients map[string]*gtsmodel.Client testApplications map[string]*gtsmodel.Application testUsers map[string]*gtsmodel.User testAccounts map[string]*gtsmodel.Account @@ -65,7 +64,6 @@ type StatusStandardTestSuite struct { func (suite *StatusStandardTestSuite) SetupSuite() { suite.testTokens = testrig.NewTestTokens() - suite.testClients = testrig.NewTestClients() suite.testApplications = testrig.NewTestApplications() suite.testUsers = testrig.NewTestUsers() suite.testAccounts = testrig.NewTestAccounts() diff --git a/internal/processing/stream/stream_test.go b/internal/processing/stream/stream_test.go index 96ea65b0f..3e5bad2b1 100644 --- a/internal/processing/stream/stream_test.go +++ b/internal/processing/stream/stream_test.go @@ -52,7 +52,7 @@ func (suite *StreamTestSuite) SetupTest() { suite.db = testrig.NewTestDB(&suite.state) suite.state.DB = suite.db suite.state.AdminActions = admin.New(suite.state.DB, &suite.state.Workers) - suite.oauthServer = testrig.NewTestOauthServer(suite.db) + suite.oauthServer = testrig.NewTestOauthServer(&suite.state) suite.streamProcessor = stream.New(&suite.state, suite.oauthServer) testrig.StandardDBSetup(suite.db, suite.testAccounts) diff --git a/internal/processing/user/user_test.go b/internal/processing/user/user_test.go index 72fd22117..46fc73206 100644 --- a/internal/processing/user/user_test.go +++ b/internal/processing/user/user_test.go @@ -54,7 +54,7 @@ func (suite *UserStandardTestSuite) SetupTest() { suite.db = testrig.NewTestDB(&suite.state) suite.state.DB = suite.db suite.state.AdminActions = admin.New(suite.state.DB, &suite.state.Workers) - suite.oauthServer = testrig.NewTestOauthServer(suite.state.DB) + suite.oauthServer = testrig.NewTestOauthServer(&suite.state) suite.sentEmails = make(map[string]string) suite.emailSender = testrig.NewEmailSender("../../../web/template/", suite.sentEmails) @@ -62,7 +62,7 @@ func (suite *UserStandardTestSuite) SetupTest() { suite.testTokens = testrig.NewTestTokens() suite.testUsers = testrig.NewTestUsers() - suite.user = user.New(&suite.state, typeutils.NewConverter(&suite.state), testrig.NewTestOauthServer(suite.db), suite.emailSender) + suite.user = user.New(&suite.state, typeutils.NewConverter(&suite.state), testrig.NewTestOauthServer(&suite.state), suite.emailSender) testrig.StandardDBSetup(suite.db, nil) } diff --git a/internal/processing/workers/workers_test.go b/internal/processing/workers/workers_test.go index b7ec54c1e..d069f0b89 100644 --- a/internal/processing/workers/workers_test.go +++ b/internal/processing/workers/workers_test.go @@ -39,7 +39,6 @@ type WorkersTestSuite struct { // standard suite models testTokens map[string]*gtsmodel.Token - testClients map[string]*gtsmodel.Client testApplications map[string]*gtsmodel.Application testUsers map[string]*gtsmodel.User testAccounts map[string]*gtsmodel.Account @@ -57,7 +56,6 @@ type WorkersTestSuite struct { func (suite *WorkersTestSuite) SetupSuite() { suite.testTokens = testrig.NewTestTokens() - suite.testClients = testrig.NewTestClients() suite.testApplications = testrig.NewTestApplications() suite.testUsers = testrig.NewTestUsers() suite.testAccounts = testrig.NewTestAccounts() diff --git a/internal/text/formatter_test.go b/internal/text/formatter_test.go index 6fd8f4d7b..07e176278 100644 --- a/internal/text/formatter_test.go +++ b/internal/text/formatter_test.go @@ -37,7 +37,6 @@ type TextStandardTestSuite struct { // standard suite models testTokens map[string]*gtsmodel.Token - testClients map[string]*gtsmodel.Client testApplications map[string]*gtsmodel.Application testUsers map[string]*gtsmodel.User testAccounts map[string]*gtsmodel.Account @@ -53,7 +52,6 @@ type TextStandardTestSuite struct { func (suite *TextStandardTestSuite) SetupSuite() { suite.testTokens = testrig.NewTestTokens() - suite.testClients = testrig.NewTestClients() suite.testApplications = testrig.NewTestApplications() suite.testUsers = testrig.NewTestUsers() suite.testAccounts = testrig.NewTestAccounts() diff --git a/internal/transport/transport_test.go b/internal/transport/transport_test.go index 61df16e52..bed683d27 100644 --- a/internal/transport/transport_test.go +++ b/internal/transport/transport_test.go @@ -50,7 +50,6 @@ type TransportTestSuite struct { // standard suite models testTokens map[string]*gtsmodel.Token - testClients map[string]*gtsmodel.Client testApplications map[string]*gtsmodel.Application testUsers map[string]*gtsmodel.User testAccounts map[string]*gtsmodel.Account @@ -60,7 +59,6 @@ type TransportTestSuite struct { func (suite *TransportTestSuite) SetupSuite() { suite.testTokens = testrig.NewTestTokens() - suite.testClients = testrig.NewTestClients() suite.testApplications = testrig.NewTestApplications() suite.testUsers = testrig.NewTestUsers() suite.testAccounts = testrig.NewTestAccounts() diff --git a/internal/typeutils/internaltofrontend.go b/internal/typeutils/internaltofrontend.go index 446fe1954..510b165d1 100644 --- a/internal/typeutils/internaltofrontend.go +++ b/internal/typeutils/internaltofrontend.go @@ -626,10 +626,12 @@ func (c *Converter) AppToAPIAppSensitive(ctx context.Context, a *gtsmodel.Applic ID: a.ID, Name: a.Name, Website: a.Website, - RedirectURI: a.RedirectURI, + RedirectURI: strings.Join(a.RedirectURIs, "\n"), + RedirectURIs: a.RedirectURIs, ClientID: a.ClientID, ClientSecret: a.ClientSecret, VapidKey: vapidKeyPair.Public, + Scopes: strings.Split(a.Scopes, " "), }, nil } diff --git a/internal/webpush/realsender_test.go b/internal/webpush/realsender_test.go index d5172c00e..28a5eae95 100644 --- a/internal/webpush/realsender_test.go +++ b/internal/webpush/realsender_test.go @@ -23,6 +23,7 @@ "net/http" "testing" "time" + // for go:linkname _ "unsafe" @@ -102,7 +103,7 @@ func (suite *RealSenderStandardTestSuite) SetupTest() { suite.transportController = testrig.NewTestTransportController(&suite.state, suite.httpClient) suite.mediaManager = testrig.NewTestMediaManager(&suite.state) suite.federator = testrig.NewTestFederator(&suite.state, suite.transportController, suite.mediaManager) - suite.oauthServer = testrig.NewTestOauthServer(suite.db) + suite.oauthServer = testrig.NewTestOauthServer(&suite.state) suite.emailSender = testrig.NewEmailSender("../../web/template/", nil) suite.webPushSender = newSenderWith( diff --git a/testrig/db.go b/testrig/db.go index d33a63f12..2f44b3777 100644 --- a/testrig/db.go +++ b/testrig/db.go @@ -68,7 +68,6 @@ >smodel.Notification{}, >smodel.RouterSession{}, >smodel.Token{}, - >smodel.Client{}, >smodel.EmojiCategory{}, >smodel.Tombstone{}, >smodel.Report{}, @@ -132,12 +131,6 @@ func StandardDBSetup(db db.DB, accounts map[string]*gtsmodel.Account) { } } - for _, v := range NewTestClients() { - if err := db.Put(ctx, v); err != nil { - log.Panic(ctx, err) - } - } - for _, v := range NewTestApplications() { if err := db.Put(ctx, v); err != nil { log.Panic(ctx, err) diff --git a/testrig/oauthserver.go b/testrig/oauthserver.go index 6d570ece3..df3caada3 100644 --- a/testrig/oauthserver.go +++ b/testrig/oauthserver.go @@ -20,11 +20,17 @@ import ( "context" - "github.com/superseriousbusiness/gotosocial/internal/db" + apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util" "github.com/superseriousbusiness/gotosocial/internal/oauth" + "github.com/superseriousbusiness/gotosocial/internal/state" ) // NewTestOauthServer returns an oauth server with the given db -func NewTestOauthServer(db db.DB) oauth.Server { - return oauth.New(context.Background(), db) +func NewTestOauthServer(state *state.State) oauth.Server { + ctx := context.Background() + return oauth.New( + ctx, + state, + apiutil.GetClientScopeHandler(ctx, state), + ) } diff --git a/testrig/processor.go b/testrig/processor.go index 478e2124c..2df7ef197 100644 --- a/testrig/processor.go +++ b/testrig/processor.go @@ -51,7 +51,7 @@ func NewTestProcessor( ), typeutils.NewConverter(state), federator, - NewTestOauthServer(state.DB), + NewTestOauthServer(state), mediaManager, state, emailSender, diff --git a/testrig/testmodels.go b/testrig/testmodels.go index 2d0d1e1ce..42b2150dc 100644 --- a/testrig/testmodels.go +++ b/testrig/testmodels.go @@ -70,6 +70,7 @@ func NewTestTokens() map[string]*gtsmodel.Token { ID: "01P9SVWS9J3SPHZQ3KCMBEN70N", ClientID: "01F8MGV8AC3NGSJW0FE8W1BV70", RedirectURI: "http://localhost:8080", + Scope: "read write push", Access: "ZTK1MWMWZDGTMGMXOS0ZY2UXLWI5ZWETMWEZYZZIYTLHMZI4", AccessCreateAt: TimeMustParse("2022-06-10T15:22:08Z"), AccessExpiresAt: TimeMustParse("2050-01-01T15:22:08Z"), @@ -79,6 +80,7 @@ func NewTestTokens() map[string]*gtsmodel.Token { ClientID: "01F8MGV8AC3NGSJW0FE8W1BV70", UserID: "01F8MGVGPHQ2D3P3X0454H54Z5", RedirectURI: "http://localhost:8080", + Scope: "read write push", Code: "ZJYYMZQ0MTQTZTU1NC0ZNJK4LWE2ZWITYTM1MDHHOTAXNJHL", CodeCreateAt: TimeMustParse("2022-06-10T15:22:08Z"), CodeExpiresAt: TimeMustParse("2050-01-01T15:22:08Z"), @@ -107,37 +109,6 @@ func NewTestTokens() map[string]*gtsmodel.Token { return tokens } -// NewTestClients returns a map of Clients keyed according to which account they are used by. -func NewTestClients() map[string]*gtsmodel.Client { - clients := map[string]*gtsmodel.Client{ - "instance_application": { - ID: "01AY6P665V14JJR0AFVRT7311Y", - Secret: "baedee87-6d00-4cf5-87b9-4d78ee58ef01", - Domain: "http://localhost:8080", - UserID: "", - }, - "admin_account": { - ID: "01F8MGWSJCND9BWBD4WGJXBM93", - Secret: "dda8e835-2c9c-4bd2-9b8b-77c2e26d7a7a", - Domain: "http://localhost:8080", - UserID: "01F8MGWYWKVKS3VS8DV1AMYPGE", // admin_account - }, - "local_account_1": { - ID: "01F8MGV8AC3NGSJW0FE8W1BV70", - Secret: "c3724c74-dc3b-41b2-a108-0ea3d8399830", - Domain: "http://localhost:8080", - UserID: "01F8MGVGPHQ2D3P3X0454H54Z5", // local_account_1 - }, - "local_account_2": { - ID: "01F8MGW47HN8ZXNHNZ7E47CDMQ", - Secret: "8f5603a5-c721-46cd-8f1b-2e368f51379f", - Domain: "http://localhost:8080", - UserID: "01F8MH1VYJAE00TVVGMM5JNJ8X", // local_account_2 - }, - } - return clients -} - // NewTestApplications returns a map of applications keyed to which number application they are. func NewTestApplications() map[string]*gtsmodel.Application { apps := map[string]*gtsmodel.Application{ @@ -145,7 +116,7 @@ func NewTestApplications() map[string]*gtsmodel.Application { ID: "01HT5P2YHDMPAAD500NDAY8JW1", Name: "localhost:8080 instance application", Website: "http://localhost:8080", - RedirectURI: "http://localhost:8080", + RedirectURIs: []string{"http://localhost:8080"}, ClientID: "01AY6P665V14JJR0AFVRT7311Y", // instance account ID ClientSecret: "baedee87-6d00-4cf5-87b9-4d78ee58ef01", Scopes: "write:accounts", @@ -154,28 +125,28 @@ func NewTestApplications() map[string]*gtsmodel.Application { ID: "01F8MGXQRHYF5QPMTMXP78QC2F", Name: "superseriousbusiness", Website: "https://superserious.business", - RedirectURI: "http://localhost:8080", + RedirectURIs: []string{"http://localhost:8080"}, ClientID: "01F8MGWSJCND9BWBD4WGJXBM93", // admin client ClientSecret: "dda8e835-2c9c-4bd2-9b8b-77c2e26d7a7a", // admin client - Scopes: "read write follow push", + Scopes: "read write push", }, "application_1": { ID: "01F8MGY43H3N2C8EWPR2FPYEXG", Name: "really cool gts application", Website: "https://reallycool.app", - RedirectURI: "http://localhost:8080", + RedirectURIs: []string{"http://localhost:8080"}, ClientID: "01F8MGV8AC3NGSJW0FE8W1BV70", // client_1 ClientSecret: "c3724c74-dc3b-41b2-a108-0ea3d8399830", // client_1 - Scopes: "read write follow push", + Scopes: "read write push", }, "application_2": { ID: "01F8MGYG9E893WRHW0TAEXR8GJ", Name: "kindaweird", Website: "https://kindaweird.app", - RedirectURI: "http://localhost:8080", + RedirectURIs: []string{"http://localhost:8080"}, ClientID: "01F8MGW47HN8ZXNHNZ7E47CDMQ", // client_2 ClientSecret: "8f5603a5-c721-46cd-8f1b-2e368f51379f", // client_2 - Scopes: "read write follow push", + Scopes: "read write push", }, } return apps diff --git a/testrig/teststructs.go b/testrig/teststructs.go index 7d5c3caab..f8eb1b3ed 100644 --- a/testrig/teststructs.go +++ b/testrig/teststructs.go @@ -82,7 +82,7 @@ func SetupTestStructs( transportController := NewTestTransportController(&state, httpClient) mediaManager := NewTestMediaManager(&state) federator := NewTestFederator(&state, transportController, mediaManager) - oauthServer := NewTestOauthServer(db) + oauthServer := NewTestOauthServer(&state) emailSender := NewEmailSender(rTemplatePath, nil) webPushSender := NewWebPushMockSender()