diff --git a/api/proto b/api/proto index 90719c2e8..b4647568a 160000 --- a/api/proto +++ b/api/proto @@ -1 +1 @@ -Subproject commit 90719c2e8abf13e91f437f201e5d259210195213 +Subproject commit b4647568a5587b5e80f64415dad35bc7a7e8edef diff --git a/api/server/v1/marshaler.go b/api/server/v1/marshaler.go index 2c3732729..0644d6202 100644 --- a/api/server/v1/marshaler.go +++ b/api/server/v1/marshaler.go @@ -19,10 +19,10 @@ import ( "net/url" "strings" "time" + "unsafe" "github.com/grpc-ecosystem/grpc-gateway/v2/runtime" jsoniter "github.com/json-iterator/go" - "github.com/tigrisdata/tigris/util" spb "google.golang.org/genproto/googleapis/rpc/status" "google.golang.org/protobuf/proto" ) @@ -259,10 +259,7 @@ func (x *ImportRequest) UnmarshalJSON(data []byte) error { return err } - x.Documents = make([][]byte, len(docs)) - for i := 0; i < len(docs); i++ { - x.Documents[i] = docs[i] - } + x.Documents = RawMessageToByte(docs) continue default: continue @@ -314,10 +311,7 @@ func (x *InsertRequest) UnmarshalJSON(data []byte) error { return err } - x.Documents = make([][]byte, len(docs)) - for i := 0; i < len(docs); i++ { - x.Documents[i] = docs[i] - } + x.Documents = RawMessageToByte(docs) continue default: continue @@ -358,10 +352,7 @@ func (x *ReplaceRequest) UnmarshalJSON(data []byte) error { return err } - x.Documents = make([][]byte, len(docs)) - for i := 0; i < len(docs); i++ { - x.Documents[i] = docs[i] - } + x.Documents = RawMessageToByte(docs) continue case "options": v = &x.Options @@ -552,7 +543,7 @@ func (x *CreateOrUpdateCollectionsRequest) UnmarshalJSON(data []byte) error { return err } - x.Schemas = util.RawMessageToByte(schemas) + x.Schemas = RawMessageToByte(schemas) continue case "options": @@ -1178,3 +1169,8 @@ func unmarshalRollup(data []byte) (*RollupFunction, error) { return result, nil } + +func RawMessageToByte(arr []jsoniter.RawMessage) [][]byte { + ptr := unsafe.Pointer(&arr) + return *(*[][]byte)(ptr) +} diff --git a/api/server/v1/tx.go b/api/server/v1/tx.go index 09feb2d39..e2ef998d5 100644 --- a/api/server/v1/tx.go +++ b/api/server/v1/tx.go @@ -72,6 +72,12 @@ const ( ListAppKeysMethodName = apiMethodPrefix + "ListAppKeys" RotateAppKeySecretMethodName = apiMethodPrefix + "RotateAppKeySecret" + CreateGlobalAppKeyMethodName = apiMethodPrefix + "CreateGlobalAppKey" + UpdateGlobalAppKeyMethodName = apiMethodPrefix + "UpdateGlobalAppKey" + DeleteGlobalAppKeyMethodName = apiMethodPrefix + "DeleteGlobalAppKey" + ListGlobalAppKeysMethodName = apiMethodPrefix + "ListGlobalAppKeys" + RotateGlobalAppKeySecretMethodName = apiMethodPrefix + "RotateGlobalAppKeySecret" + // Auth. GetAccessTokenMethodName = authMethodPrefix + "GetAccessToken" CreateInvitationsMethodName = authMethodPrefix + "CreateInvitations" diff --git a/server/main.go b/server/main.go index 1030ce302..6963b3150 100644 --- a/server/main.go +++ b/server/main.go @@ -30,6 +30,7 @@ import ( "github.com/tigrisdata/tigris/server/services/v1/billing" "github.com/tigrisdata/tigris/server/tracing" "github.com/tigrisdata/tigris/server/transaction" + "github.com/tigrisdata/tigris/server/types" "github.com/tigrisdata/tigris/store/kv" "github.com/tigrisdata/tigris/store/search" "github.com/tigrisdata/tigris/util" @@ -57,6 +58,7 @@ func mainWithCode() int { log.Info().Msgf("Environment: '%v'", config.GetEnvironment()) log.Info().Msgf("Number of CPUs: %v", runtime.NumCPU()) log.Info().Msgf("Server Type: '%v'", config.DefaultConfig.Server.Type) + log.Info().Msgf("My Origin: '%v'", types.MyOrigin) defaultConfig := &config.DefaultConfig closerFunc, err := tracing.InitTracer(defaultConfig) diff --git a/server/middleware/authz.go b/server/middleware/authz.go index a3246e3ff..e90284182 100644 --- a/server/middleware/authz.go +++ b/server/middleware/authz.go @@ -179,6 +179,11 @@ var ( api.DeleteAppKeyMethodName, api.ListAppKeysMethodName, api.RotateAppKeySecretMethodName, + api.CreateGlobalAppKeyMethodName, + api.UpdateGlobalAppKeyMethodName, + api.DeleteGlobalAppKeyMethodName, + api.ListGlobalAppKeysMethodName, + api.RotateGlobalAppKeySecretMethodName, api.IndexCollection, api.SearchIndexCollectionMethodName, diff --git a/server/middleware/authz_test.go b/server/middleware/authz_test.go index 4aba6d06a..184d29d1c 100644 --- a/server/middleware/authz_test.go +++ b/server/middleware/authz_test.go @@ -52,6 +52,11 @@ func TestAuthzOwnerRole(t *testing.T) { require.True(t, isAuthorized(api.DeleteAppKeyMethodName, ownerRoleName)) require.True(t, isAuthorized(api.ListAppKeysMethodName, ownerRoleName)) require.True(t, isAuthorized(api.RotateAppKeySecretMethodName, ownerRoleName)) + require.True(t, isAuthorized(api.CreateGlobalAppKeyMethodName, ownerRoleName)) + require.True(t, isAuthorized(api.UpdateGlobalAppKeyMethodName, ownerRoleName)) + require.True(t, isAuthorized(api.DeleteGlobalAppKeyMethodName, ownerRoleName)) + require.True(t, isAuthorized(api.ListGlobalAppKeysMethodName, ownerRoleName)) + require.True(t, isAuthorized(api.RotateGlobalAppKeySecretMethodName, ownerRoleName)) require.True(t, isAuthorized(api.IndexCollection, ownerRoleName)) require.True(t, isAuthorized(api.SearchIndexCollectionMethodName, ownerRoleName)) @@ -185,6 +190,12 @@ func TestAuthzEditorRole(t *testing.T) { require.False(t, isAuthorized(api.CreateNamespaceMethodName, editorRoleName)) require.False(t, isAuthorized(api.ListNamespacesMethodName, editorRoleName)) require.False(t, isAuthorized(api.DeleteNamespaceMethodName, editorRoleName)) + + require.False(t, isAuthorized(api.CreateGlobalAppKeyMethodName, editorRoleName)) + require.False(t, isAuthorized(api.UpdateGlobalAppKeyMethodName, editorRoleName)) + require.False(t, isAuthorized(api.DeleteGlobalAppKeyMethodName, editorRoleName)) + require.False(t, isAuthorized(api.ListGlobalAppKeysMethodName, editorRoleName)) + require.False(t, isAuthorized(api.RotateGlobalAppKeySecretMethodName, editorRoleName)) } func TestAuthzReadOnlyRole(t *testing.T) { @@ -253,4 +264,9 @@ func TestAuthzReadOnlyRole(t *testing.T) { require.False(t, isAuthorized(api.CreateNamespaceMethodName, readOnlyRoleName)) require.False(t, isAuthorized(api.ListNamespacesMethodName, readOnlyRoleName)) require.False(t, isAuthorized(api.DeleteNamespaceMethodName, readOnlyRoleName)) + require.False(t, isAuthorized(api.CreateGlobalAppKeyMethodName, readOnlyRoleName)) + require.False(t, isAuthorized(api.UpdateGlobalAppKeyMethodName, readOnlyRoleName)) + require.False(t, isAuthorized(api.DeleteGlobalAppKeyMethodName, readOnlyRoleName)) + require.False(t, isAuthorized(api.ListGlobalAppKeysMethodName, readOnlyRoleName)) + require.False(t, isAuthorized(api.RotateGlobalAppKeySecretMethodName, readOnlyRoleName)) } diff --git a/server/services/v1/api.go b/server/services/v1/api.go index 096e05ee0..46835b590 100644 --- a/server/services/v1/api.go +++ b/server/services/v1/api.go @@ -47,6 +47,7 @@ const ( databasePathPattern = fullProjectPath + "/database/*" applicationPathPattern = fullProjectPath + "/apps/*" + appsPath = "/apps/*" infoPath = "/info" metricsPath = "/metrics" ) @@ -140,6 +141,9 @@ func (s *apiService) RegisterHTTP(router chi.Router, inproc *inprocgrpc.Channel) router.HandleFunc(apiPathPrefix+infoPath, func(w http.ResponseWriter, r *http.Request) { mux.ServeHTTP(w, r) }) + router.HandleFunc(apiPathPrefix+appsPath, func(w http.ResponseWriter, r *http.Request) { + mux.ServeHTTP(w, r) + }) if config.DefaultConfig.Metrics.Enabled { router.Handle(metricsPath, metrics.Reporter.HTTPHandler()) @@ -632,3 +636,23 @@ func (s *apiService) ListAppKeys(ctx context.Context, req *api.ListAppKeysReques func (s *apiService) RotateAppKeySecret(ctx context.Context, req *api.RotateAppKeyRequest) (*api.RotateAppKeyResponse, error) { return s.authProvider.RotateAppKey(ctx, req) } + +func (s *apiService) CreateGlobalAppKey(ctx context.Context, req *api.CreateGlobalAppKeyRequest) (*api.CreateGlobalAppKeyResponse, error) { + return s.authProvider.CreateGlobalAppKey(ctx, req) +} + +func (s *apiService) UpdateGlobalAppKey(ctx context.Context, req *api.UpdateGlobalAppKeyRequest) (*api.UpdateGlobalAppKeyResponse, error) { + return s.authProvider.UpdateGlobalAppKey(ctx, req) +} + +func (s *apiService) DeleteGlobalAppKey(ctx context.Context, req *api.DeleteGlobalAppKeyRequest) (*api.DeleteGlobalAppKeyResponse, error) { + return s.authProvider.DeleteGlobalAppKey(ctx, req) +} + +func (s *apiService) ListGlobalAppKeys(ctx context.Context, req *api.ListGlobalAppKeysRequest) (*api.ListGlobalAppKeysResponse, error) { + return s.authProvider.ListGlobalAppKeys(ctx, req) +} + +func (s *apiService) RotateGlobalAppKeySecret(ctx context.Context, req *api.RotateGlobalAppKeySecretRequest) (*api.RotateGlobalAppKeySecretResponse, error) { + return s.authProvider.RotateGlobalAppKeySecret(ctx, req) +} diff --git a/server/services/v1/auth/auth0.go b/server/services/v1/auth/auth0.go index d5f1dac10..4a2bfe70e 100644 --- a/server/services/v1/auth/auth0.go +++ b/server/services/v1/auth/auth0.go @@ -308,6 +308,26 @@ func (a *auth0) DeleteAppKeys(ctx context.Context, project string) error { return nil } +func (*auth0) CreateGlobalAppKey(_ context.Context, _ *api.CreateGlobalAppKeyRequest) (*api.CreateGlobalAppKeyResponse, error) { + return nil, errors.Internal("auth0 implementation doesn't support it") +} + +func (*auth0) UpdateGlobalAppKey(_ context.Context, _ *api.UpdateGlobalAppKeyRequest) (*api.UpdateGlobalAppKeyResponse, error) { + return nil, errors.Internal("auth0 implementation doesn't support it") +} + +func (*auth0) RotateGlobalAppKeySecret(_ context.Context, _ *api.RotateGlobalAppKeySecretRequest) (*api.RotateGlobalAppKeySecretResponse, error) { + return nil, errors.Internal("auth0 implementation doesn't support it") +} + +func (*auth0) DeleteGlobalAppKey(_ context.Context, _ *api.DeleteGlobalAppKeyRequest) (*api.DeleteGlobalAppKeyResponse, error) { + return nil, errors.Internal("auth0 implementation doesn't support it") +} + +func (*auth0) ListGlobalAppKeys(_ context.Context, _ *api.ListGlobalAppKeysRequest) (*api.ListGlobalAppKeysResponse, error) { + return nil, errors.Internal("auth0 implementation doesn't support it") +} + func validateOwnershipAuth0(ctx context.Context, operationName string, appId string, a *auth0) (*management.Client, string, error) { client, err := a.Management.Client.Read(appId) if err != nil { diff --git a/server/services/v1/auth/gotrue.go b/server/services/v1/auth/gotrue.go index fa42c7c32..23a96b48f 100644 --- a/server/services/v1/auth/gotrue.go +++ b/server/services/v1/auth/gotrue.go @@ -95,20 +95,17 @@ type UserAppData struct { Project string `json:"tigris_project"` } -func (g *gotrue) CreateAppKey(ctx context.Context, req *api.CreateAppKeyRequest) (*api.CreateAppKeyResponse, error) { - clientId := generateClientId(g) - clientSecret := generateClientSecret(g) - +func _createAppKey(ctx context.Context, clientId string, clientSecret string, g *gotrue, keyName string, keyDescription string, project string) (string, int64, error) { currentSub, err := GetCurrentSub(ctx) if err != nil { log.Err(err).Msg("Failed to create application: reason - unable to extract current sub") - return nil, errors.Internal("Failed to create application: reason = %s", err.Error()) + return "", 0, errors.Internal("Failed to create application: reason = %s", err.Error()) } currentNamespace, err := request.GetNamespace(ctx) if err != nil { log.Err(err).Msg("Failed to create application: reason - unable to extract current namespace") - return nil, errors.Internal("Failed to create applications: reason = %s", err.Error()) + return "", 0, errors.Internal("Failed to create applications: reason = %s", err.Error()) } // make gotrue call @@ -120,18 +117,18 @@ func (g *gotrue) CreateAppKey(ctx context.Context, req *api.CreateAppKeyRequest) CreatedAt: creationTime, CreatedBy: currentSub, TigrisNamespace: currentNamespace, - Name: req.GetName(), - Description: req.GetDescription(), - Project: req.GetProject(), + Name: keyName, + Description: keyDescription, + Project: project, }, }) if err != nil { log.Err(err).Msg("Failed to create user") - return nil, errors.Internal("Failed to create user") + return "", 0, errors.Internal("Failed to create user") } err = createUser(ctx, payloadBytes, g.AuthConfig.PrimaryAudience, AppKeyUser, g) if err != nil { - return nil, err + return "", 0, err } log.Info(). Str("namespace", currentNamespace). @@ -139,6 +136,21 @@ func (g *gotrue) CreateAppKey(ctx context.Context, req *api.CreateAppKeyRequest) Str("client_id", clientId). Str(Component, AppKey). Msg("appkey created") + return currentSub, creationTime, nil +} + +func (g *gotrue) CreateAppKey(ctx context.Context, req *api.CreateAppKeyRequest) (*api.CreateAppKeyResponse, error) { + if req.GetProject() == "" { + return nil, errors.InvalidArgument("Project must be specified") + } + clientId := generateClientId(g) + clientSecret := generateClientSecret(g) + + currentSub, creationTime, err := _createAppKey(ctx, clientId, clientSecret, g, req.GetName(), req.GetDescription(), req.GetProject()) + if err != nil { + return nil, err + } + return &api.CreateAppKeyResponse{ CreatedAppKey: &api.AppKey{ Id: clientId, @@ -152,14 +164,35 @@ func (g *gotrue) CreateAppKey(ctx context.Context, req *api.CreateAppKeyRequest) }, nil } -func (g *gotrue) UpdateAppKey(ctx context.Context, req *api.UpdateAppKeyRequest) (*api.UpdateAppKeyResponse, error) { - email := fmt.Sprintf("%s%s", req.GetId(), g.AuthConfig.Gotrue.UsernameSuffix) +func (g *gotrue) CreateGlobalAppKey(ctx context.Context, req *api.CreateGlobalAppKeyRequest) (*api.CreateGlobalAppKeyResponse, error) { + clientId := generateClientId(g) + clientSecret := generateClientSecret(g) + + currentSub, creationTime, err := _createAppKey(ctx, clientId, clientSecret, g, req.GetName(), req.GetDescription(), "") + if err != nil { + return nil, err + } + + return &api.CreateGlobalAppKeyResponse{ + CreatedAppKey: &api.GlobalAppKey{ + Id: clientId, + Name: req.GetName(), + Description: req.GetDescription(), + Secret: clientSecret, + CreatedAt: creationTime, + CreatedBy: currentSub, + }, + }, nil +} + +func _updateAppKey(ctx context.Context, g *gotrue, id string, name string, description string) error { + email := fmt.Sprintf("%s%s", id, g.AuthConfig.Gotrue.UsernameSuffix) updateAppKeyUrl := fmt.Sprintf("%s/admin/users/%s", g.AuthConfig.Gotrue.URL, email) currentSub, err := GetCurrentSub(ctx) if err != nil { log.Err(err).Msg("Couldn't resolve current sub") - return nil, errors.Internal("Failed to update app key") + return errors.Internal("Failed to update app key") } newAppMetadata := UserAppData{ @@ -167,19 +200,19 @@ func (g *gotrue) UpdateAppKey(ctx context.Context, req *api.UpdateAppKeyRequest) UpdatedBy: currentSub, } - if req.GetName() != "" { - newAppMetadata.Name = req.GetName() + if name != "" { + newAppMetadata.Name = name } - if req.GetDescription() != "" { - newAppMetadata.Description = req.GetDescription() + if description != "" { + newAppMetadata.Description = description } appMetadataMap := make(map[string]UserAppData) appMetadataMap["app_metadata"] = newAppMetadata payloadBytes, err := jsoniter.Marshal(appMetadataMap) if err != nil { log.Err(err).Msg("Failed to marshal payload") - return nil, errors.Internal("Unable to update app key") + return errors.Internal("Unable to update app key") } payloadBytesReader := bytes.NewReader(payloadBytes) @@ -187,12 +220,12 @@ func (g *gotrue) UpdateAppKey(ctx context.Context, req *api.UpdateAppKeyRequest) updateAppKeyReq, err := http.NewRequestWithContext(ctx, http.MethodPut, updateAppKeyUrl, payloadBytesReader) if err != nil { log.Err(err).Msg("Failed to construct updateAppKeyReq") - return nil, errors.Internal("Unable to update app key") + return errors.Internal("Unable to update app key") } adminAccessToken, _, err := getGotrueAdminAccessToken(ctx, g) if err != nil { log.Err(err).Msg("Failed to get admin access token") - return nil, errors.Internal("Failed to update app key: couldn't get admin access token") + return errors.Internal("Failed to update app key: couldn't get admin access token") } updateAppKeyReq.Header.Add("Authorization", fmt.Sprintf("bearer %s", adminAccessToken)) updateAppKeyReq.Header.Add("Content-Type", "application/json") @@ -200,13 +233,29 @@ func (g *gotrue) UpdateAppKey(ctx context.Context, req *api.UpdateAppKeyRequest) updateAppKeyRes, err := ctxhttp.Do(ctx, client, updateAppKeyReq) if err != nil { log.Err(err).Msg("Failed to update app key - failed to make call to gotrue") - return nil, errors.Internal("Failed to update app key") + return errors.Internal("Failed to update app key") } defer updateAppKeyRes.Body.Close() if updateAppKeyRes.StatusCode != http.StatusOK { log.Error().Int("status", updateAppKeyRes.StatusCode).Msg("Received non OK status code to update user.") - return nil, errors.Internal("Failed to update app key") + return errors.Internal("Failed to update app key") + } + log.Info(). + Str("sub", currentSub). + Str("client_id", id). + Str(Component, AppKey). + Msg("appkey updated") + return nil +} + +func (g *gotrue) UpdateAppKey(ctx context.Context, req *api.UpdateAppKeyRequest) (*api.UpdateAppKeyResponse, error) { + if req.GetProject() == "" { + return nil, errors.InvalidArgument("Project must be specified") + } + err := _updateAppKey(ctx, g, req.GetId(), req.GetName(), req.GetDescription()) + if err != nil { + return nil, err } result := &api.UpdateAppKeyResponse{ UpdatedAppKey: &api.AppKey{ @@ -219,22 +268,36 @@ func (g *gotrue) UpdateAppKey(ctx context.Context, req *api.UpdateAppKeyRequest) if req.GetDescription() != "" { result.UpdatedAppKey.Description = req.GetDescription() } - log.Info(). - Str("sub", currentSub). - Str("client_id", req.GetId()). - Str(Component, AppKey). - Msg("appkey updated") return result, nil } -func (g *gotrue) RotateAppKey(ctx context.Context, req *api.RotateAppKeyRequest) (*api.RotateAppKeyResponse, error) { - email := fmt.Sprintf("%s%s", req.GetId(), g.AuthConfig.Gotrue.UsernameSuffix) +func (g *gotrue) UpdateGlobalAppKey(ctx context.Context, req *api.UpdateGlobalAppKeyRequest) (*api.UpdateGlobalAppKeyResponse, error) { + err := _updateAppKey(ctx, g, req.GetId(), req.GetName(), req.GetDescription()) + if err != nil { + return nil, err + } + result := &api.UpdateGlobalAppKeyResponse{ + UpdatedAppKey: &api.GlobalAppKey{ + Id: req.GetId(), + }, + } + if req.GetName() != "" { + result.UpdatedAppKey.Name = req.GetName() + } + if req.GetDescription() != "" { + result.UpdatedAppKey.Description = req.GetDescription() + } + return result, nil +} + +func _rotateAppKeySecret(ctx context.Context, g *gotrue, id string) (string, error) { + email := fmt.Sprintf("%s%s", id, g.AuthConfig.Gotrue.UsernameSuffix) updateAppKeyUrl := fmt.Sprintf("%s/admin/users/%s", g.AuthConfig.Gotrue.URL, email) currentSub, err := GetCurrentSub(ctx) if err != nil { log.Err(err).Msg("Couldn't resolve current sub") - return nil, errors.Internal("Failed to update app key") + return "", errors.Internal("Failed to update app key") } newSecret := generateClientSecret(g) @@ -246,7 +309,7 @@ func (g *gotrue) RotateAppKey(ctx context.Context, req *api.RotateAppKeyRequest) payloadBytes, err := jsoniter.Marshal(payload) if err != nil { log.Err(err).Msg("Failed to marshal payload") - return nil, errors.Internal("Unable to update app key") + return "", errors.Internal("Unable to update app key") } payloadBytesReader := bytes.NewReader(payloadBytes) @@ -255,12 +318,12 @@ func (g *gotrue) RotateAppKey(ctx context.Context, req *api.RotateAppKeyRequest) updateAppKeyReq, err := http.NewRequestWithContext(ctx, http.MethodPut, updateAppKeyUrl, payloadBytesReader) if err != nil { log.Err(err).Msg("Failed to construct updateAppKeyReq") - return nil, errors.Internal("Unable to update app key") + return "", errors.Internal("Unable to update app key") } adminAccessToken, _, err := getGotrueAdminAccessToken(ctx, g) if err != nil { log.Err(err).Msg("Failed to get admin access token") - return nil, errors.Internal("Failed to update app key: couldn't get admin access token") + return "", errors.Internal("Failed to update app key: couldn't get admin access token") } updateAppKeyReq.Header.Add("Authorization", fmt.Sprintf("bearer %s", adminAccessToken)) updateAppKeyReq.Header.Add("Content-Type", "application/json") @@ -268,73 +331,132 @@ func (g *gotrue) RotateAppKey(ctx context.Context, req *api.RotateAppKeyRequest) updateAppKeyRes, err := ctxhttp.Do(ctx, client, updateAppKeyReq) if err != nil { log.Err(err).Msg("Failed to update app key - failed to make call to gotrue") - return nil, errors.Internal("Failed to update app key") + return "", errors.Internal("Failed to update app key") } defer updateAppKeyRes.Body.Close() if updateAppKeyRes.StatusCode != http.StatusOK { log.Error().Int("status", updateAppKeyRes.StatusCode).Msg("Received non OK status code to update user.") - return nil, errors.Internal("Failed to update app key") + return "", errors.Internal("Failed to update app key") + } + + log.Info(). + Str("sub", currentSub). + Str("client_id", id). + Str(Component, AppKey). + Msg("appkey rotated") + + return newSecret, nil +} + +func (g *gotrue) RotateAppKey(ctx context.Context, req *api.RotateAppKeyRequest) (*api.RotateAppKeyResponse, error) { + if req.GetProject() == "" { + return nil, errors.InvalidArgument("Project must be specified") + } + newSecret, err := _rotateAppKeySecret(ctx, g, req.GetId()) + if err != nil { + return nil, err } + result := &api.RotateAppKeyResponse{ AppKey: &api.AppKey{ Id: req.GetId(), Secret: newSecret, }, } + return result, nil +} - log.Info(). - Str("sub", currentSub). - Str("client_id", req.GetId()). - Str(Component, AppKey). - Msg("appkey rotated") +func (g *gotrue) RotateGlobalAppKeySecret(ctx context.Context, req *api.RotateGlobalAppKeySecretRequest) (*api.RotateGlobalAppKeySecretResponse, error) { + newSecret, err := _rotateAppKeySecret(ctx, g, req.GetId()) + if err != nil { + return nil, err + } + + result := &api.RotateGlobalAppKeySecretResponse{ + AppKey: &api.GlobalAppKey{ + Id: req.GetId(), + Secret: newSecret, + }, + } return result, nil } -func (g *gotrue) DeleteAppKey(ctx context.Context, req *api.DeleteAppKeyRequest) (*api.DeleteAppKeyResponse, error) { +func _deleteAppKey(ctx context.Context, g *gotrue, id string) error { // TODO: verify ownership // get admin access token adminAccessToken, _, err := getGotrueAdminAccessToken(ctx, g) if err != nil { - return nil, err + return err } // make external call - deleteUserUrl := fmt.Sprintf("%s/admin/users/%s%s", g.AuthConfig.Gotrue.URL, req.GetId(), g.AuthConfig.Gotrue.UsernameSuffix) + deleteUserUrl := fmt.Sprintf("%s/admin/users/%s%s", g.AuthConfig.Gotrue.URL, id, g.AuthConfig.Gotrue.UsernameSuffix) client := &http.Client{} deleteUserReq, err := http.NewRequestWithContext(ctx, http.MethodDelete, deleteUserUrl, nil) if err != nil { log.Err(err).Msg("Failed to form request to delete user from gotrue") - return nil, errors.Internal("Failed to form request to delete app key") + return errors.Internal("Failed to form request to delete app key") } deleteUserReq.Header.Add("Authorization", fmt.Sprintf("bearer %s", adminAccessToken)) deleteUserRes, err := ctxhttp.Do(ctx, client, deleteUserReq) if err != nil { log.Err(err).Msg("Failed to delete user from gotrue") - return nil, errors.Internal("Failed to delete user from gotrue") + return errors.Internal("Failed to delete user from gotrue") } defer deleteUserRes.Body.Close() if deleteUserRes.StatusCode != http.StatusOK { log.Error().Int("status", deleteUserRes.StatusCode).Msg("Received non OK status code to delete user") - return nil, errors.Internal("Received non OK status code to delete user") + return errors.Internal("Received non OK status code to delete user") } currentSub, _ := GetCurrentSub(ctx) log.Info(). Str("sub", currentSub). - Str("client_id", req.GetId()). + Str("client_id", id). Str(Component, AppKey). Msg("appkey deleted") + return nil +} + +func (g *gotrue) DeleteAppKey(ctx context.Context, req *api.DeleteAppKeyRequest) (*api.DeleteAppKeyResponse, error) { + if req.GetProject() == "" { + return nil, errors.InvalidArgument("Project must be specified") + } + err := _deleteAppKey(ctx, g, req.GetId()) + if err != nil { + return nil, err + } return &api.DeleteAppKeyResponse{ Deleted: true, }, nil } -func (g *gotrue) ListAppKeys(ctx context.Context, req *api.ListAppKeysRequest) (*api.ListAppKeysResponse, error) { +func (g *gotrue) DeleteGlobalAppKey(ctx context.Context, req *api.DeleteGlobalAppKeyRequest) (*api.DeleteGlobalAppKeyResponse, error) { + err := _deleteAppKey(ctx, g, req.GetId()) + if err != nil { + return nil, err + } + return &api.DeleteGlobalAppKeyResponse{ + Deleted: true, + }, nil +} + +type appKeyInternal struct { + Id string + Name string + Description string + Secret string + CreatedBy string + CreatedAt int64 + Project string +} + +func _listAppKeys(ctx context.Context, g *gotrue, project string) ([]*appKeyInternal, error) { currentSub, err := GetCurrentSub(ctx) if err != nil { return nil, errors.Internal("Failed to list applications: reason = %s", err.Error()) @@ -352,7 +474,7 @@ func (g *gotrue) ListAppKeys(ctx context.Context, req *api.ListAppKeysRequest) ( } // make external call - getUsersUrl := fmt.Sprintf("%s/admin/users?created_by=%s&tigris_namespace=%s&tigris_project=%s&page=1&per_page=5000", g.AuthConfig.Gotrue.URL, currentSub, currentNamespace, req.GetProject()) + getUsersUrl := fmt.Sprintf("%s/admin/users?created_by=%s&tigris_namespace=%s&tigris_project=%s&page=1&per_page=5000", g.AuthConfig.Gotrue.URL, currentSub, currentNamespace, project) client := &http.Client{} getUsersReq, err := http.NewRequestWithContext(ctx, http.MethodGet, getUsersUrl, nil) if err != nil { @@ -394,7 +516,7 @@ func (g *gotrue) ListAppKeys(ctx context.Context, req *api.ListAppKeysRequest) ( return nil, errors.Internal("Failed to parse getUsers response") } - appKeys := make([]*api.AppKey, len(users)) + appKeys := make([]*appKeyInternal, len(users)) for i, user := range users { var email, clientSecret string err := jsoniter.Unmarshal(user["email"], &email) @@ -429,7 +551,7 @@ func (g *gotrue) ListAppKeys(ctx context.Context, req *api.ListAppKeysRequest) ( createdAtMillis = readDate(createdAtStr) } - appKey := api.AppKey{ + appKey := appKeyInternal{ Id: clientId, Name: appMetadata.Name, Description: appMetadata.Description, @@ -440,11 +562,52 @@ func (g *gotrue) ListAppKeys(ctx context.Context, req *api.ListAppKeysRequest) ( } appKeys[i] = &appKey } + return appKeys, nil +} + +func (g *gotrue) ListAppKeys(ctx context.Context, req *api.ListAppKeysRequest) (*api.ListAppKeysResponse, error) { + appKeysInternal, err := _listAppKeys(ctx, g, req.GetProject()) + if err != nil { + return nil, errors.Internal("Failed to delete app keys") + } + appKeys := make([]*api.AppKey, len(appKeysInternal)) + for i, internalAppKey := range appKeysInternal { + appKeys[i] = &api.AppKey{ + Id: internalAppKey.Id, + Name: internalAppKey.Name, + Description: internalAppKey.Description, + Secret: internalAppKey.Secret, + CreatedAt: internalAppKey.CreatedAt, + CreatedBy: internalAppKey.CreatedBy, + Project: internalAppKey.Project, + } + } return &api.ListAppKeysResponse{ AppKeys: appKeys, }, nil } +func (g *gotrue) ListGlobalAppKeys(ctx context.Context, _ *api.ListGlobalAppKeysRequest) (*api.ListGlobalAppKeysResponse, error) { + appKeysInternal, err := _listAppKeys(ctx, g, "") + if err != nil { + return nil, errors.Internal("Failed to delete app keys") + } + globalAppKeys := make([]*api.GlobalAppKey, len(appKeysInternal)) + for i, internalAppKey := range appKeysInternal { + globalAppKeys[i] = &api.GlobalAppKey{ + Id: internalAppKey.Id, + Name: internalAppKey.Name, + Description: internalAppKey.Description, + Secret: internalAppKey.Secret, + CreatedAt: internalAppKey.CreatedAt, + CreatedBy: internalAppKey.CreatedBy, + } + } + return &api.ListGlobalAppKeysResponse{ + AppKeys: globalAppKeys, + }, nil +} + func (g *gotrue) DeleteAppKeys(ctx context.Context, project string) error { // TODO make it transactional on gotrue side listAppKeysResp, err := g.ListAppKeys(ctx, &api.ListAppKeysRequest{Project: project}) diff --git a/server/services/v1/auth/noop.go b/server/services/v1/auth/noop.go index 008b3f7a2..b310861eb 100644 --- a/server/services/v1/auth/noop.go +++ b/server/services/v1/auth/noop.go @@ -50,3 +50,23 @@ func (noop) ListAppKeys(_ context.Context, _ *api.ListAppKeysRequest) (*api.List func (noop) DeleteAppKeys(_ context.Context, _ string) error { return errors.Internal("authentication not enabled on this server") } + +func (noop) CreateGlobalAppKey(_ context.Context, _ *api.CreateGlobalAppKeyRequest) (*api.CreateGlobalAppKeyResponse, error) { + return nil, errors.Internal("authentication not enabled on this server") +} + +func (noop) UpdateGlobalAppKey(_ context.Context, _ *api.UpdateGlobalAppKeyRequest) (*api.UpdateGlobalAppKeyResponse, error) { + return nil, errors.Internal("authentication not enabled on this server") +} + +func (noop) RotateGlobalAppKeySecret(_ context.Context, _ *api.RotateGlobalAppKeySecretRequest) (*api.RotateGlobalAppKeySecretResponse, error) { + return nil, errors.Internal("authentication not enabled on this server") +} + +func (noop) DeleteGlobalAppKey(_ context.Context, _ *api.DeleteGlobalAppKeyRequest) (*api.DeleteGlobalAppKeyResponse, error) { + return nil, errors.Internal("authentication not enabled on this server") +} + +func (noop) ListGlobalAppKeys(_ context.Context, _ *api.ListGlobalAppKeysRequest) (*api.ListGlobalAppKeysResponse, error) { + return nil, errors.Internal("authentication not enabled on this server") +} diff --git a/server/services/v1/auth/provider.go b/server/services/v1/auth/provider.go index 75acccc5b..5cd6b93ab 100644 --- a/server/services/v1/auth/provider.go +++ b/server/services/v1/auth/provider.go @@ -53,6 +53,12 @@ type Provider interface { DeleteAppKey(ctx context.Context, req *api.DeleteAppKeyRequest) (*api.DeleteAppKeyResponse, error) ListAppKeys(ctx context.Context, req *api.ListAppKeysRequest) (*api.ListAppKeysResponse, error) DeleteAppKeys(ctx context.Context, project string) error + + CreateGlobalAppKey(ctx context.Context, req *api.CreateGlobalAppKeyRequest) (*api.CreateGlobalAppKeyResponse, error) + UpdateGlobalAppKey(ctx context.Context, req *api.UpdateGlobalAppKeyRequest) (*api.UpdateGlobalAppKeyResponse, error) + RotateGlobalAppKeySecret(ctx context.Context, req *api.RotateGlobalAppKeySecretRequest) (*api.RotateGlobalAppKeySecretResponse, error) + DeleteGlobalAppKey(ctx context.Context, req *api.DeleteGlobalAppKeyRequest) (*api.DeleteGlobalAppKeyResponse, error) + ListGlobalAppKeys(ctx context.Context, req *api.ListGlobalAppKeysRequest) (*api.ListGlobalAppKeysResponse, error) } func NewProvider(userstore *metadata.UserSubspace, txMgr *transaction.Manager) Provider { diff --git a/test/docker/docker-compose.yml b/test/docker/docker-compose.yml index 838e49d11..f94337c5d 100644 --- a/test/docker/docker-compose.yml +++ b/test/docker/docker-compose.yml @@ -80,7 +80,7 @@ services: ports: - "8081:8081" command: > - bash -c '[ ! -f /etc/foundationdb/initialized ] && fdbcli --exec "configure new single memory" && touch /etc/foundationdb/initialized; sleep 5; + bash -c '[ ! -f /etc/foundationdb/initialized ] && fdbcli --exec "configure new single memory" && touch /etc/foundationdb/initialized; /server/service' depends_on: - tigris_db diff --git a/test/v1/server/auth_test.go b/test/v1/server/auth_test.go index ad715f18e..bbf96343e 100644 --- a/test/v1/server/auth_test.go +++ b/test/v1/server/auth_test.go @@ -205,6 +205,106 @@ func TestGoTrueAuthProvider(t *testing.T) { require.True(t, deletedResponse.Raw()) } +func TestGlobalAppKeys(t *testing.T) { + e2 := expectLow(t, config.GetBaseURL2()) + token := readToken(t, RSATokenFilePath) + + createTestNamespace(t, token) + + // create app key + createGlobalAppKeyPayload := Map{ + "name": "test_key", + "description": "This key is used for integration test purpose.", + } + + createdAppKey := e2.POST(globalAppKeysOperation("create")). + WithHeader(Authorization, Bearer+token).WithJSON(createGlobalAppKeyPayload). + Expect(). + Status(http.StatusOK). + JSON(). + Object().Value("created_app_key") + require.NotNil(t, createdAppKey) + id := createdAppKey.Object().Value("id").String() + secret := createdAppKey.Object().Value("secret").String() + + name := createdAppKey.Object().Value("name").String() + description := createdAppKey.Object().Value("description").String() + + require.Equal(t, "test_key", name.Raw()) + require.Equal(t, "This key is used for integration test purpose.", description.Raw()) + require.True(t, int(id.Length().Raw()) == 30+len(auth.ClientIdPrefix)) // length + prefix + require.True(t, int(secret.Length().Raw()) == 50+len(auth.ClientSecretPrefix)) // length + prefix + + // update + updateGlobalAppKeyPayload := Map{ + "id": id.Raw(), + "description": "[updated]This key is used for integration test purpose.", + } + updatedAppKey := e2.PUT(globalAppKeysOperation("update")). + WithHeader(Authorization, Bearer+token).WithJSON(updateGlobalAppKeyPayload). + Expect(). + Status(http.StatusOK). + JSON(). + Object().Value("updated_app_key") + // updates only in description + require.Equal(t, id.Raw(), updatedAppKey.Object().Value("id").Raw()) + require.Equal(t, "[updated]This key is used for integration test purpose.", updatedAppKey.Object().Value("description").Raw()) + + // rotate + rotateGlobalAppKeyPayload := Map{ + "id": id.Raw(), + } + rotatedKey := e2.POST(globalAppKeysOperation("rotate")). + WithHeader(Authorization, Bearer+token).WithJSON(rotateGlobalAppKeyPayload). + Expect(). + Status(http.StatusOK). + JSON(). + Object().Value("app_key") + require.Equal(t, id.Raw(), rotatedKey.Object().Value("id").Raw()) + require.NotEqual(t, secret.Raw(), rotatedKey.Object().Value("secret").Raw()) + require.True(t, len(rotatedKey.Object().Value("secret").String().Raw()) == 50+len(auth.ClientSecretPrefix)) + + // list + globalAppKeys := e2.GET(globalAppKeysOperation("get")). + WithHeader(Authorization, Bearer+token). + Expect(). + Status(http.StatusOK). + JSON(). + Object().Value("app_keys").Array() + + found := false + for i := 0; i < int(globalAppKeys.Length().Raw()); i++ { + k := globalAppKeys.Element(i) + if k.Object().Value("id").Raw() == id.Raw() { + found = true + require.NotEqual(t, secret.Raw(), rotatedKey.Object().Value("secret").Raw()) + } + } + + require.True(t, found) + + retrievedAppKey := globalAppKeys.Element(0) + require.Equal(t, id.Raw(), retrievedAppKey.Object().Value("id").String().Raw()) + require.NotNil(t, retrievedAppKey.Object().Value("secret").String().Raw()) + require.Equal(t, name.Raw(), retrievedAppKey.Object().Value("name").String().Raw()) + require.Equal(t, "[updated]This key is used for integration test purpose.", retrievedAppKey.Object().Value("description").String().Raw()) + require.True(t, strings.HasPrefix(retrievedAppKey.Object().Value("created_by").String().Raw(), "gt|")) + + // delete + deleteGlobalAppKeyPayload := Map{ + "id": id.Raw(), + } + deletedResponse := e2.DELETE(globalAppKeysOperation("delete")). + WithHeader(Authorization, Bearer+token).WithJSON(deleteGlobalAppKeyPayload). + Expect(). + Status(http.StatusOK). + JSON(). + Object(). + Value("deleted"). + Boolean() + require.True(t, deletedResponse.Raw()) +} + func TestMultipleAppsCreation(t *testing.T) { testStartTime := time.Now() @@ -367,6 +467,50 @@ func TestCreateAccessToken(t *testing.T) { createProject2(t, "new-project-3", accessToken).Status(http.StatusOK) } +func TestCreateGlobalAccessToken(t *testing.T) { + e2 := expectLow(t, config.GetBaseURL2()) + token := readToken(t, RSATokenFilePath) + createTestNamespace(t, token) + + createGlobalAppKeyPayload := Map{ + "name": "test_key", + "description": "This key is used for integration test purpose.", + } + + createdAppKey := e2.POST(globalAppKeysOperation("create")). + WithHeader(Authorization, Bearer+token).WithJSON(createGlobalAppKeyPayload). + Expect(). + Status(http.StatusOK). + JSON(). + Object().Value("created_app_key") + require.NotNil(t, createdAppKey) + + id := createdAppKey.Object().Value("id").String() + secret := createdAppKey.Object().Value("secret").String() + + getAccessTokenResponse := e2.POST(getAuthToken()). + WithFormField("client_id", id.Raw()). + WithFormField("client_secret", secret.Raw()). + WithFormField("grant_type", "client_credentials"). + Expect() + getAccessTokenResponse.Status(http.StatusOK) + + accessToken := getAccessTokenResponse.JSON().Object().Value("access_token").String().Raw() + require.True(t, accessToken != "") + require.NotNil(t, getAccessTokenResponse.JSON().Object().Value("expires_in")) + + // use access token + createProject2(t, "TestCreateGlobalAccessToken-project-1", accessToken).Status(http.StatusOK) + + deleteProject2(t, "TestCreateGlobalAccessToken-project-1", token) + // use access token bypassing auth caches + _ = e2.POST(getProjectURL("TestCreateGlobalAccessToken-project-2", "create")). + WithHeader(Authorization, Bearer+accessToken). + WithHeader(api.HeaderBypassAuthCache, "true"). + Expect(). + Status(http.StatusOK) +} + func TestCreateAccessTokenUsingInvalidCreds(t *testing.T) { e2 := expectLow(t, config.GetBaseURL2()) getAccessTokenResponse := e2.POST(getAuthToken()). diff --git a/test/v1/server/integration_test.go b/test/v1/server/integration_test.go index 362ea1b2b..8ad197980 100644 --- a/test/v1/server/integration_test.go +++ b/test/v1/server/integration_test.go @@ -282,6 +282,13 @@ func appKeysOperation(project string, operation string) string { return fmt.Sprintf("/v1/projects/%s/apps/keys/%s", project, operation) } +func globalAppKeysOperation(operation string) string { + if operation == "get" { + return "/v1/apps/keys" + } + return fmt.Sprintf("/v1/apps/keys/%s", operation) +} + func getAuthToken() string { return "/v1/auth/token" }