diff --git a/.github/actions/install/action.yaml b/.github/actions/install/action.yaml index e5608fba71..eb7e4ede28 100644 --- a/.github/actions/install/action.yaml +++ b/.github/actions/install/action.yaml @@ -14,13 +14,13 @@ runs: - name: Setup Go uses: actions/setup-go@v5 with: - go-version-file: ./apps/agent/go.mod - cache-dependency-path: ./apps/agent/go.sum + go-version-file: ./go/go.mod + cache-dependency-path: ./go/go.sum - run: go mod download if: ${{ inputs.go == 'true' }} shell: bash - working-directory: ./apps/agent + working-directory: ./go - name: Install tparse run: go install github.com/mfridman/tparse@latest diff --git a/go/apps/api/integration/multi_node_ratelimiting/run.go b/go/apps/api/integration/multi_node_ratelimiting/run.go index 31a66acbfb..b1020846d5 100644 --- a/go/apps/api/integration/multi_node_ratelimiting/run.go +++ b/go/apps/api/integration/multi_node_ratelimiting/run.go @@ -85,8 +85,8 @@ func RunRateLimitTest( // Maximum theoretical allowed requests across all windows maxAllowed := math.Min(numWindows*float64(limit), float64(totalRequests)) - // Set acceptance thresholds with 5% tolerance - upperLimit := int(maxAllowed * 1.1) + // Set acceptance thresholds with 20% tolerance + upperLimit := int(maxAllowed * 1.2) lowerLimit := int(maxAllowed * 0.95) // Special case: When request rate is below the limit, diff --git a/go/apps/api/openapi/gen.go b/go/apps/api/openapi/gen.go index 529e0b0ee6..040dcf2f8f 100644 --- a/go/apps/api/openapi/gen.go +++ b/go/apps/api/openapi/gen.go @@ -12,6 +12,15 @@ const ( RootKeyScopes = "rootKey.Scopes" ) +// ApisCreateApiResponseData defines model for ApisCreateApiResponseData. +type ApisCreateApiResponseData struct { + // ApiId The id of the API + ApiId string `json:"apiId"` + + // Name The name of the API + Name string `json:"name"` +} + // BadRequestErrorDetails defines model for BadRequestErrorDetails. type BadRequestErrorDetails struct { // Detail A human-readable explanation specific to this occurrence of the problem. @@ -185,15 +194,36 @@ type UnauthorizedErrorResponse struct { Meta Meta `json:"meta"` } +// V2ApisCreateApiRequestBody defines model for V2ApisCreateApiRequestBody. +type V2ApisCreateApiRequestBody struct { + // Name The name for your API. This is not customer facing. + Name string `json:"name"` +} + +// V2ApisCreateApiResponseBody defines model for V2ApisCreateApiResponseBody. +type V2ApisCreateApiResponseBody struct { + Data ApisCreateApiResponseData `json:"data"` + Meta Meta `json:"meta"` +} + // V2IdentitiesCreateIdentityRequestBody defines model for V2IdentitiesCreateIdentityRequestBody. type V2IdentitiesCreateIdentityRequestBody struct { // ExternalId The id of this identity in your system. + // + // This usually comes from your authentication provider and could be a userId, organisationId or even an email. + // It does not matter what you use, as long as it uniquely identifies something in your application. + // + // `externalId`s are unique across your workspace and therefore a `CONFLICT` error is returned when you try to create duplicates. ExternalId string `json:"externalId"` // Meta Attach metadata to this identity that you need to have access to when verifying a key. + // + // This will be returned as part of the `verifyKey` response. Meta *map[string]interface{} `json:"meta,omitempty"` // Ratelimits Attach ratelimits to this identity. + // + // When verifying keys, you can specify which limits you want to use and all keys attached to this identity, will share the limits. Ratelimits *[]V2Ratelimit `json:"ratelimits,omitempty"` } @@ -339,6 +369,9 @@ type ValidationError struct { Message string `json:"message"` } +// CreateApiJSONRequestBody defines body for CreateApi for application/json ContentType. +type CreateApiJSONRequestBody = V2ApisCreateApiRequestBody + // V2IdentitiesCreateIdentityJSONRequestBody defines body for V2IdentitiesCreateIdentity for application/json ContentType. type V2IdentitiesCreateIdentityJSONRequestBody = V2IdentitiesCreateIdentityRequestBody diff --git a/go/apps/api/openapi/openapi.json b/go/apps/api/openapi/openapi.json index 672c6d4223..47f553fcf4 100644 --- a/go/apps/api/openapi/openapi.json +++ b/go/apps/api/openapi/openapi.json @@ -574,9 +574,6 @@ }, "meta": { "type": "object", - "additionalProperties": { - "nullable": true - }, "description": "Attach metadata to this identity that you need to have access to when verifying a key.\n\nThis will be returned as part of the `verifyKey` response.\n" }, "ratelimits": { @@ -631,6 +628,45 @@ "$ref": "#/components/schemas/IdentitiesCreateIdentityResponseData" } } + }, + "V2ApisCreateApiRequestBody": { + "type": "object", + "required": ["name"], + "properties": { + "name": { + "type": "string", + "minLength": 3, + "description": "The name for your API. This is not customer facing.", + "example": "my-api" + } + }, + "additionalProperties": false + }, + "ApisCreateApiResponseData": { + "type": "object", + "properties": { + "apiId": { + "description": "The id of the API", + "type": "string" + }, + "name": { + "description": "The name of the API", + "type": "string" + } + }, + "required": ["apiId", "name"] + }, + "V2ApisCreateApiResponseBody": { + "type": "object", + "required": ["meta", "data"], + "properties": { + "meta": { + "$ref": "#/components/schemas/Meta" + }, + "data": { + "$ref": "#/components/schemas/ApisCreateApiResponseData" + } + } } } }, @@ -1135,6 +1171,79 @@ } } }, + "/v2/apis.createApi": { + "post": { + "tags": ["apis"], + "operationId": "createApi", + "security": [ + { + "rootKey": [] + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/V2ApisCreateApiRequestBody" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/V2ApisCreateApiResponseBody" + } + } + }, + "description": "Successfully created API" + }, + "400": { + "description": "Bad request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BadRequestErrorResponse" + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedErrorResponse" + } + } + } + }, + "403": { + "description": "Forbidden", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ForbiddenErrorResponse" + } + } + } + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InternalServerErrorResponse" + } + } + }, + "description": "Internal server error" + } + } + } + }, "/v2/liveness": { "get": { "tags": ["liveness"], diff --git a/go/apps/api/routes/v2_apis_create_api/200_test.go b/go/apps/api/routes/v2_apis_create_api/200_test.go new file mode 100644 index 0000000000..1d009b49fd --- /dev/null +++ b/go/apps/api/routes/v2_apis_create_api/200_test.go @@ -0,0 +1,202 @@ +package handler_test + +import ( + "context" + "database/sql" + "fmt" + "net/http" + "testing" + "time" + + "github.com/stretchr/testify/require" + handler "github.com/unkeyed/unkey/go/apps/api/routes/v2_apis_create_api" + "github.com/unkeyed/unkey/go/pkg/db" + "github.com/unkeyed/unkey/go/pkg/testutil" + "github.com/unkeyed/unkey/go/pkg/uid" +) + +func TestCreateApiSuccessfully(t *testing.T) { + ctx := context.Background() + h := testutil.NewHarness(t) + + route := handler.New(handler.Services{ + DB: h.DB, + Keys: h.Keys, + Logger: h.Logger, + Permissions: h.Permissions, + Auditlogs: h.Auditlogs, + }) + + h.Register(route) + + rootKey := h.CreateRootKey(h.Resources().UserWorkspace.ID, "api.*.create_api") + headers := http.Header{ + "Content-Type": {"application/json"}, + "Authorization": {fmt.Sprintf("Bearer %s", rootKey)}, + } + + // Test creating an API manually via DB to verify queries + t.Run("insert api via DB", func(t *testing.T) { + keyAuthID := uid.New(uid.KeyAuthPrefix) + err := db.Query.InsertKeyring(ctx, h.DB.RW(), db.InsertKeyringParams{ + ID: keyAuthID, + WorkspaceID: h.Resources().UserWorkspace.ID, + CreatedAtM: time.Now().UnixMilli(), + DefaultPrefix: sql.NullString{Valid: false, String: ""}, + DefaultBytes: sql.NullInt32{Valid: false, Int32: 0}, + }) + require.NoError(t, err) + + apiID := uid.New(uid.APIPrefix) + apiName := "test-api-db" + err = db.Query.InsertApi(ctx, h.DB.RW(), db.InsertApiParams{ + ID: apiID, + Name: apiName, + WorkspaceID: h.Resources().UserWorkspace.ID, + AuthType: db.NullApisAuthType{Valid: true, ApisAuthType: db.ApisAuthTypeKey}, + KeyAuthID: sql.NullString{Valid: true, String: keyAuthID}, + CreatedAtM: time.Now().UnixMilli(), + }) + require.NoError(t, err) + + api, err := db.Query.FindApiById(ctx, h.DB.RO(), apiID) + require.NoError(t, err) + require.Equal(t, apiName, api.Name) + require.Equal(t, h.Resources().UserWorkspace.ID, api.WorkspaceID) + require.True(t, api.KeyAuthID.Valid) + require.Equal(t, keyAuthID, api.KeyAuthID.String) + }) + + // Test creating a basic API + t.Run("create basic api", func(t *testing.T) { + apiName := "test-api-basic" + req := handler.Request{ + Name: apiName, + } + + res := testutil.CallRoute[handler.Request, handler.Response](h, route, headers, req) + require.Equal(t, 200, res.Status, "expected 200, received: %#v", res) + require.NotNil(t, res.Body) + require.NotEmpty(t, res.Body.Data.ApiId) + require.Equal(t, apiName, res.Body.Data.Name) + + // Verify the API in the database + api, err := db.Query.FindApiById(ctx, h.DB.RO(), res.Body.Data.ApiId) + require.NoError(t, err) + require.Equal(t, apiName, api.Name) + require.Equal(t, h.Resources().UserWorkspace.ID, api.WorkspaceID) + require.True(t, api.AuthType.Valid) + require.Equal(t, db.ApisAuthTypeKey, api.AuthType.ApisAuthType) + require.True(t, api.KeyAuthID.Valid) + require.NotEmpty(t, api.KeyAuthID.String) + require.False(t, api.DeletedAtM.Valid) + + // Verify the key auth was created + keyAuth, err := db.Query.FindKeyringByID(ctx, h.DB.RO(), api.KeyAuthID.String) + require.NoError(t, err) + require.Equal(t, h.Resources().UserWorkspace.ID, keyAuth.WorkspaceID) + require.False(t, keyAuth.DeletedAtM.Valid) + + // Verify the audit log was created + auditLogs, err := db.Query.FindAuditLogTargetById(ctx, h.DB.RO(), res.Body.Data.ApiId) + require.NoError(t, err) + require.GreaterOrEqual(t, len(auditLogs), 1) + + var foundCreateEvent bool + for _, log := range auditLogs { + if log.AuditLog.Event == "api.create" { + foundCreateEvent = true + require.Equal(t, h.Resources().UserWorkspace.ID, log.AuditLog.WorkspaceID) + break + } + } + require.True(t, foundCreateEvent, "Should find an api.create audit log event") + }) + + // Test creating multiple APIs + t.Run("create multiple apis", func(t *testing.T) { + apiNames := []string{"api-1", "api-2", "api-3"} + apiIds := make([]string, len(apiNames)) + + for i, name := range apiNames { + req := handler.Request{ + Name: name, + } + + res := testutil.CallRoute[handler.Request, handler.Response](h, route, headers, req) + require.Equal(t, 200, res.Status) + require.NotEmpty(t, res.Body.Data.ApiId) + require.Equal(t, name, res.Body.Data.Name) + + apiIds[i] = res.Body.Data.ApiId + } + + // Verify all APIs were created with unique IDs + apiIdSet := make(map[string]bool) + for _, apiId := range apiIds { + apiIdSet[apiId] = true + } + require.Equal(t, len(apiNames), len(apiIdSet), "All API IDs should be unique") + + // Verify each API in the database + for i, apiId := range apiIds { + api, err := db.Query.FindApiById(ctx, h.DB.RO(), apiId) + require.NoError(t, err) + require.Equal(t, apiNames[i], api.Name) + } + }) + + // Test with a longer API name + t.Run("create api with long name", func(t *testing.T) { + apiName := "my-super-awesome-production-api-for-customer-management-and-analytics" + req := handler.Request{ + Name: apiName, + } + + res := testutil.CallRoute[handler.Request, handler.Response](h, route, headers, req) + require.Equal(t, 200, res.Status) + require.Equal(t, apiName, res.Body.Data.Name) + + api, err := db.Query.FindApiById(ctx, h.DB.RO(), res.Body.Data.ApiId) + require.NoError(t, err) + require.Equal(t, apiName, api.Name) + }) + + // Test with special characters in name + t.Run("create api with special characters", func(t *testing.T) { + apiName := "special_api-123!@#$%^&*()" + req := handler.Request{ + Name: apiName, + } + + res := testutil.CallRoute[handler.Request, handler.Response](h, route, headers, req) + require.Equal(t, 200, res.Status) + require.Equal(t, apiName, res.Body.Data.Name) + + api, err := db.Query.FindApiById(ctx, h.DB.RO(), res.Body.Data.ApiId) + require.NoError(t, err) + require.Equal(t, apiName, api.Name) + }) + + t.Run("create api with UUID name", func(t *testing.T) { + apiName := uid.New("uuid-test-") // Using uid.New to generate a unique ID + req := handler.Request{ + Name: apiName, + } + + res := testutil.CallRoute[handler.Request, handler.Response](h, route, headers, req) + require.Equal(t, 200, res.Status, "expected 200, received: %#v", res) + require.NotNil(t, res.Body) + require.NotEmpty(t, res.Body.Data.ApiId) + require.Equal(t, apiName, res.Body.Data.Name) + + // Verify the API in the database + api, err := db.Query.FindApiById(ctx, h.DB.RO(), res.Body.Data.ApiId) + require.NoError(t, err) + require.Equal(t, apiName, api.Name) + + // Verify delete protection is false (specifically tested in TypeScript) + require.True(t, api.DeleteProtection.Valid) + require.False(t, api.DeleteProtection.Bool) + }) +} diff --git a/go/apps/api/routes/v2_apis_create_api/400_test.go b/go/apps/api/routes/v2_apis_create_api/400_test.go new file mode 100644 index 0000000000..c0e5cb882a --- /dev/null +++ b/go/apps/api/routes/v2_apis_create_api/400_test.go @@ -0,0 +1,83 @@ +package handler_test + +import ( + "fmt" + "net/http" + "testing" + + "github.com/stretchr/testify/require" + "github.com/unkeyed/unkey/go/apps/api/openapi" + handler "github.com/unkeyed/unkey/go/apps/api/routes/v2_apis_create_api" + "github.com/unkeyed/unkey/go/pkg/testutil" +) + +func TestCreateApi_BadRequest(t *testing.T) { + h := testutil.NewHarness(t) + + route := handler.New(handler.Services{ + DB: h.DB, + Keys: h.Keys, + Logger: h.Logger, + Permissions: h.Permissions, + Auditlogs: h.Auditlogs, + }) + + h.Register(route) + + rootKey := h.CreateRootKey(h.Resources().UserWorkspace.ID, "api.*.create_api") + headers := http.Header{ + "Content-Type": {"application/json"}, + "Authorization": {fmt.Sprintf("Bearer %s", rootKey)}, + } + + // Test with name too short + t.Run("name too short", func(t *testing.T) { + req := handler.Request{ + Name: "ab", // Name should be at least 3 characters + } + + res := testutil.CallRoute[handler.Request, handler.Response](h, route, headers, req) + require.Equal(t, http.StatusBadRequest, res.Status) + }) + + // Test with missing name + t.Run("missing name", func(t *testing.T) { + // Using empty struct to simulate missing name + req := handler.Request{} + + res := testutil.CallRoute[handler.Request, handler.Response](h, route, headers, req) + require.Equal(t, http.StatusBadRequest, res.Status) + }) + + // Test with empty name + t.Run("empty name", func(t *testing.T) { + req := handler.Request{ + Name: "", + } + + res := testutil.CallRoute[handler.Request, handler.Response](h, route, headers, req) + require.Equal(t, http.StatusBadRequest, res.Status) + }) + // Test detailed error response structure + t.Run("verify error response structure", func(t *testing.T) { + req := handler.Request{ + Name: "ab", // Name should be at least 3 characters + } + + res := testutil.CallRoute[handler.Request, openapi.BadRequestErrorResponse](h, route, headers, req) + require.Equal(t, http.StatusBadRequest, res.Status) + + // Verify the error response structure (similar to TypeScript test) + require.NotEmpty(t, res.Body.Meta.RequestId) + require.NotEmpty(t, res.Body.Error) + require.Equal(t, "https://unkey.com/docs/errors/bad_request", res.Body.Error.Type) + require.Equal(t, res.Body.Error.Title, "Bad Request") + + // Check that the error message contains information about the name length + require.Equal(t, "POST request body for '/v2/apis.createApi' failed to validate schema", res.Body.Error.Detail) + require.Greater(t, len(res.Body.Error.Errors), 0) + require.Equal(t, "/properties/name/minLength", res.Body.Error.Errors[0].Location) + require.Equal(t, "minLength: got 2, want 3", res.Body.Error.Errors[0].Message) + }) + +} diff --git a/go/apps/api/routes/v2_apis_create_api/401_test.go b/go/apps/api/routes/v2_apis_create_api/401_test.go new file mode 100644 index 0000000000..366d16a1a7 --- /dev/null +++ b/go/apps/api/routes/v2_apis_create_api/401_test.go @@ -0,0 +1,61 @@ +package handler_test + +import ( + "fmt" + "net/http" + "testing" + + "github.com/stretchr/testify/require" + handler "github.com/unkeyed/unkey/go/apps/api/routes/v2_apis_create_api" + "github.com/unkeyed/unkey/go/pkg/testutil" + "github.com/unkeyed/unkey/go/pkg/uid" +) + +func TestCreateApi_Unauthorized(t *testing.T) { + h := testutil.NewHarness(t) + + route := handler.New(handler.Services{ + DB: h.DB, + Keys: h.Keys, + Logger: h.Logger, + Permissions: h.Permissions, + Auditlogs: h.Auditlogs, + }) + + h.Register(route) + + // Invalid authorization token + t.Run("invalid auth token", func(t *testing.T) { + headers := http.Header{ + "Content-Type": {"application/json"}, + "Authorization": {"Bearer invalid_token"}, + } + + req := handler.Request{ + Name: "test-api", + } + + res := testutil.CallRoute[handler.Request, handler.Response](h, route, headers, req) + require.Equal(t, http.StatusUnauthorized, res.Status) + }) + + // Test with another workspace (example) + t.Run("wrong workspace", func(t *testing.T) { + // Create key for a different workspace + otherWorkspaceId := uid.New(uid.WorkspacePrefix) + otherRootKey := h.CreateRootKey(otherWorkspaceId, "api.*.create_api") + + otherHeaders := http.Header{ + "Content-Type": {"application/json"}, + "Authorization": {fmt.Sprintf("Bearer %s", otherRootKey)}, + } + + req := handler.Request{ + Name: "test-api", + } + + res := testutil.CallRoute[handler.Request, handler.Response](h, route, otherHeaders, req) + + require.Equal(t, http.StatusUnauthorized, res.Status) + }) +} diff --git a/go/apps/api/routes/v2_apis_create_api/403_test.go b/go/apps/api/routes/v2_apis_create_api/403_test.go new file mode 100644 index 0000000000..56420d6784 --- /dev/null +++ b/go/apps/api/routes/v2_apis_create_api/403_test.go @@ -0,0 +1,86 @@ +package handler_test + +import ( + "context" + "fmt" + "net/http" + "testing" + + "github.com/stretchr/testify/require" + handler "github.com/unkeyed/unkey/go/apps/api/routes/v2_apis_create_api" + "github.com/unkeyed/unkey/go/pkg/db" + "github.com/unkeyed/unkey/go/pkg/testutil" +) + +func TestCreateApi_Forbidden(t *testing.T) { + h := testutil.NewHarness(t) + + route := handler.New(handler.Services{ + DB: h.DB, + Keys: h.Keys, + Logger: h.Logger, + Permissions: h.Permissions, + Auditlogs: h.Auditlogs, + }) + + h.Register(route) + + // Create a root key with insufficient permissions + rootKey := h.CreateRootKey(h.Resources().UserWorkspace.ID, "identity.*.create_identity") // Not api.*.create_api + headers := http.Header{ + "Content-Type": {"application/json"}, + "Authorization": {fmt.Sprintf("Bearer %s", rootKey)}, + } + + t.Run("insufficient permissions", func(t *testing.T) { + req := handler.Request{ + Name: "test-api", + } + + res := testutil.CallRoute[handler.Request, handler.Response](h, route, headers, req) + require.Equal(t, http.StatusForbidden, res.Status) + }) + + // Test with various permission combinations (matching TypeScript tests) + t.Run("permission combinations", func(t *testing.T) { + testCases := []struct { + name string + permissions []string + shouldPass bool + }{ + {name: "specific permission", permissions: []string{"api.*.create_api"}, shouldPass: true}, + {name: "specific permission and more", permissions: []string{"some.other.permission", "xxx", "api.*.create_api", "another.permission"}, shouldPass: true}, + {name: "insufficient permission", permissions: []string{"api.*.read_api"}, shouldPass: false}, + {name: "unrelated permission", permissions: []string{"identity.*.create_identity"}, shouldPass: false}, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + // Create a root key with the specific permissions + permRootKey := h.CreateRootKey(h.Resources().UserWorkspace.ID, tc.permissions...) + permHeaders := http.Header{ + "Content-Type": {"application/json"}, + "Authorization": {fmt.Sprintf("Bearer %s", permRootKey)}, + } + + req := handler.Request{ + Name: "test-api-permissions", + } + + res := testutil.CallRoute[handler.Request, handler.Response](h, route, permHeaders, req) + + if tc.shouldPass { + require.Equal(t, 200, res.Status, "Expected 200 for permission: %v, got: %s", tc.permissions, res.RawBody) + require.NotEmpty(t, res.Body.Data.ApiId) + + // Verify the API in the database + api, err := db.Query.FindApiById(context.Background(), h.DB.RO(), res.Body.Data.ApiId) + require.NoError(t, err) + require.Equal(t, req.Name, api.Name) + } else { + require.Equal(t, http.StatusForbidden, res.Status, "Expected 403 for permission: %v", tc.permissions) + } + }) + } + }) +} diff --git a/go/apps/api/routes/v2_apis_create_api/handler.go b/go/apps/api/routes/v2_apis_create_api/handler.go new file mode 100644 index 0000000000..97d063c92c --- /dev/null +++ b/go/apps/api/routes/v2_apis_create_api/handler.go @@ -0,0 +1,172 @@ +package handler + +import ( + "context" + "database/sql" + "errors" + "fmt" + "net/http" + "time" + + "github.com/unkeyed/unkey/go/apps/api/openapi" + "github.com/unkeyed/unkey/go/internal/services/auditlogs" + "github.com/unkeyed/unkey/go/internal/services/keys" + "github.com/unkeyed/unkey/go/internal/services/permissions" + "github.com/unkeyed/unkey/go/pkg/auditlog" + "github.com/unkeyed/unkey/go/pkg/db" + "github.com/unkeyed/unkey/go/pkg/fault" + "github.com/unkeyed/unkey/go/pkg/otel/logging" + "github.com/unkeyed/unkey/go/pkg/rbac" + "github.com/unkeyed/unkey/go/pkg/uid" + "github.com/unkeyed/unkey/go/pkg/zen" +) + +type Request = openapi.V2ApisCreateApiRequestBody +type Response = openapi.V2ApisCreateApiResponseBody + +type Services struct { + Logger logging.Logger + DB db.Database + Keys keys.KeyService + Permissions permissions.PermissionService + Auditlogs auditlogs.AuditLogService +} + +func New(svc Services) zen.Route { + return zen.NewRoute("POST", "/v2/apis.createApi", func(ctx context.Context, s *zen.Session) error { + auth, err := svc.Keys.VerifyRootKey(ctx, s) + if err != nil { + return err + } + + var req Request + err = s.BindBody(&req) + if err != nil { + return fault.Wrap(err, + fault.WithTag(fault.INTERNAL_SERVER_ERROR), + fault.WithDesc("invalid request body", "The request body is invalid."), + ) + } + + permissions, err := svc.Permissions.Check( + ctx, + auth.KeyID, + rbac.Or( + + rbac.T(rbac.Tuple{ + ResourceType: rbac.Api, + ResourceID: "*", + Action: rbac.CreateAPI, + }), + ), + ) + if err != nil { + return fault.Wrap(err, + fault.WithTag(fault.INTERNAL_SERVER_ERROR), + fault.WithDesc("unable to check permissions", "We're unable to check the permissions of your key."), + ) + } + + if !permissions.Valid { + return fault.New("insufficient permissions", + fault.WithTag(fault.INSUFFICIENT_PERMISSIONS), + fault.WithDesc(permissions.Message, permissions.Message), + ) + } + + tx, err := svc.DB.RW().Begin(ctx) + if err != nil { + return fault.Wrap(err, + fault.WithTag(fault.DATABASE_ERROR), + fault.WithDesc("database failed to create transaction", "Unable to start database transaction."), + ) + } + + defer func() { + rollbackErr := tx.Rollback() + if rollbackErr != nil && !errors.Is(rollbackErr, sql.ErrTxDone) { + svc.Logger.Error("rollback failed", "requestId", s.RequestID(), "error", rollbackErr) + } + }() + + keyAuthId := uid.New(uid.KeyAuthPrefix) + err = db.Query.InsertKeyring(ctx, tx, db.InsertKeyringParams{ + ID: keyAuthId, + WorkspaceID: auth.AuthorizedWorkspaceID, + CreatedAtM: time.Now().UnixMilli(), + DefaultPrefix: sql.NullString{Valid: false, String: ""}, + DefaultBytes: sql.NullInt32{Valid: false, Int32: 0}, + StoreEncryptedKeys: false, + }) + if err != nil { + return fault.Wrap(err, + fault.WithTag(fault.INTERNAL_SERVER_ERROR), + fault.WithDesc("unable to create key auth", "We're unable to create key authentication for the API."), + ) + } + + apiId := uid.New(uid.APIPrefix) + err = db.Query.InsertApi(ctx, tx, db.InsertApiParams{ + ID: apiId, + Name: req.Name, + WorkspaceID: auth.AuthorizedWorkspaceID, + AuthType: db.NullApisAuthType{Valid: true, ApisAuthType: db.ApisAuthTypeKey}, + KeyAuthID: sql.NullString{Valid: true, String: keyAuthId}, + CreatedAtM: time.Now().UnixMilli(), + }) + if err != nil { + return fault.Wrap(err, + fault.WithTag(fault.INTERNAL_SERVER_ERROR), + fault.WithDesc("unable to create api", "We're unable to create the API."), + ) + } + + err = svc.Auditlogs.Insert(ctx, tx, []auditlog.AuditLog{ + { + WorkspaceID: auth.AuthorizedWorkspaceID, + Event: auditlog.APICreateEvent, + Display: fmt.Sprintf("Created API %s", apiId), + ActorID: auth.KeyID, + ActorName: "root key", + ActorMeta: nil, + Bucket: auditlogs.DEFAULT_BUCKET, + ActorType: auditlog.RootKeyActor, + RemoteIP: s.Location(), + UserAgent: s.UserAgent(), + Resources: []auditlog.AuditLogResource{ + { + ID: apiId, + Type: auditlog.APIResourceType, + Meta: nil, + Name: req.Name, + DisplayName: req.Name, + }, + }, + }, + }) + if err != nil { + return fault.Wrap(err, + fault.WithTag(fault.DATABASE_ERROR), + fault.WithDesc("database failed to insert audit logs", "Failed to insert audit logs"), + ) + } + + err = tx.Commit() + if err != nil { + return fault.Wrap(err, + fault.WithTag(fault.DATABASE_ERROR), + fault.WithDesc("database failed to commit transaction", "Failed to commit changes."), + ) + } + + return s.JSON(http.StatusOK, Response{ + Meta: openapi.Meta{ + RequestId: s.RequestID(), + }, + Data: openapi.ApisCreateApiResponseData{ + ApiId: apiId, + Name: req.Name, + }, + }) + }) +} diff --git a/go/cmd/quotacheck/main.go b/go/cmd/quotacheck/main.go index 9c56cbe40a..6a9d253aab 100644 --- a/go/cmd/quotacheck/main.go +++ b/go/cmd/quotacheck/main.go @@ -156,11 +156,11 @@ func sendSlackNotification(webhookURL string, e db.ListWorkspacesRow, used int64 }, { "type": "mrkdwn", - "text": fmt.Sprintf("*Clerk ID:*\n`%s`", e.Workspace.TenantID), + "text": fmt.Sprintf("*Organisation ID:*\n`%s`", e.Workspace.OrgID), }, { "type": "mrkdwn", - "text": fmt.Sprintf("*Organisation ID:*\n`%s`", e.Workspace.OrgID.String), + "text": fmt.Sprintf("*Stripe ID:*\n`%s`", e.Workspace.StripeCustomerID.String), }, }, }, diff --git a/go/go.mod b/go/go.mod index 5c3d4ced02..002189beb6 100644 --- a/go/go.mod +++ b/go/go.mod @@ -12,8 +12,8 @@ require ( github.com/oapi-codegen/oapi-codegen/v2 v2.4.1 github.com/ory/dockertest/v3 v3.11.0 github.com/pb33f/libopenapi v0.21.8 - github.com/pb33f/libopenapi-validator v0.3.0 - github.com/prometheus/client_golang v1.21.1 + github.com/pb33f/libopenapi-validator v0.3.1 + github.com/prometheus/client_golang v1.22.0 github.com/redis/go-redis/v9 v9.7.3 github.com/sqlc-dev/sqlc v1.28.0 github.com/stretchr/testify v1.10.0 @@ -28,6 +28,7 @@ require ( go.opentelemetry.io/otel/sdk/log v0.11.0 go.opentelemetry.io/otel/trace v1.35.0 golang.org/x/text v0.24.0 + google.golang.org/protobuf v1.36.6 ) require ( @@ -101,7 +102,7 @@ require ( github.com/pingcap/tidb/pkg/parser v0.0.0-20241203170126-9812d85d0d25 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect - github.com/prometheus/client_model v0.6.1 // indirect + github.com/prometheus/client_model v0.6.2 // indirect github.com/prometheus/common v0.63.0 // indirect github.com/prometheus/procfs v0.16.0 // indirect github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect @@ -132,17 +133,16 @@ require ( go.uber.org/atomic v1.11.0 // indirect go.uber.org/multierr v1.11.0 // indirect go.uber.org/zap v1.27.0 // indirect - golang.org/x/crypto v0.36.0 // indirect + golang.org/x/crypto v0.37.0 // indirect golang.org/x/exp v0.0.0-20250210185358-939b2ce775ac // indirect golang.org/x/mod v0.23.0 // indirect - golang.org/x/net v0.38.0 // indirect + golang.org/x/net v0.39.0 // indirect golang.org/x/sync v0.13.0 // indirect golang.org/x/sys v0.32.0 // indirect golang.org/x/tools v0.30.0 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20250404141209-ee84b53bf3d0 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20250404141209-ee84b53bf3d0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20250409194420-de1ac958c67a // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20250409194420-de1ac958c67a // indirect google.golang.org/grpc v1.71.1 // indirect - google.golang.org/protobuf v1.36.6 // indirect gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/go/go.sum b/go/go.sum index 194b82eca0..c0d2f244cb 100644 --- a/go/go.sum +++ b/go/go.sum @@ -9,6 +9,7 @@ github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg6 github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/ClickHouse/ch-go v0.65.1 h1:SLuxmLl5Mjj44/XbINsK2HFvzqup0s6rwKLFH347ZhU= github.com/ClickHouse/ch-go v0.65.1/go.mod h1:bsodgURwmrkvkBe5jw1qnGDgyITsYErfONKAHn05nv4= +github.com/ClickHouse/clickhouse-go v1.5.4 h1:cKjXeYLNWVJIx2J1K6H2CqyRmfwVJVY1OV1coaaFcI0= github.com/ClickHouse/clickhouse-go/v2 v2.34.0 h1:Y4rqkdrRHgExvC4o/NTbLdY5LFQ3LHS77/RNFxFX3Co= github.com/ClickHouse/clickhouse-go/v2 v2.34.0/go.mod h1:yioSINoRLVZkLyDzdMXPLRIqhDvel8iLBlwh6Iefso8= github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= @@ -235,6 +236,8 @@ github.com/pb33f/libopenapi v0.21.8 h1:Fi2dAogMwC6av/5n3YIo7aMOGBZH/fBMO4OnzFB3d github.com/pb33f/libopenapi v0.21.8/go.mod h1:Gc8oQkjr2InxwumK0zOBtKN9gIlv9L2VmSVIUk2YxcU= github.com/pb33f/libopenapi-validator v0.3.0 h1:xiIdPDETIPYICJn5RxD6SeGNdOBpe0ADHHW5NfNvypU= github.com/pb33f/libopenapi-validator v0.3.0/go.mod h1:NmCV/GZcDrL5slbCMbqWz/9KU3Q/qST001hiRctOXDs= +github.com/pb33f/libopenapi-validator v0.3.1 h1:7+p/y5qPlpVcJptFMpXUWGi+6D3Pdv8p8PXYmmXy/sk= +github.com/pb33f/libopenapi-validator v0.3.1/go.mod h1:R3xMZCF8mFnYww1Hf31ABAz+/QmTheLPKG4p041IR5U= github.com/perimeterx/marshmallow v1.1.5 h1:a2LALqQ1BlHM8PZblsDdidgv1mWi1DgC2UmX50IvK2s= github.com/perimeterx/marshmallow v1.1.5/go.mod h1:dsXbUu8CRzfYP5a87xpp0xq9S3u0Vchtcl8we9tYaXw= github.com/pganalyze/pg_query_go/v5 v5.1.0 h1:MlxQqHZnvA3cbRQYyIrjxEjzo560P6MyTgtlaf3pmXg= @@ -258,8 +261,12 @@ github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRI github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/prometheus/client_golang v1.21.1 h1:DOvXXTqVzvkIewV/CDPFdejpMCGeMcbGCQ8YOmu+Ibk= github.com/prometheus/client_golang v1.21.1/go.mod h1:U9NM32ykUErtVBxdvD3zfi+EuFkkaBvMb09mIfe0Zgg= +github.com/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/Y92Vm0Zc6Q= +github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0= github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= +github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= +github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= github.com/prometheus/common v0.63.0 h1:YR/EIY1o3mEFP/kZCD7iDMnLPlGyuU2Gb3HIcXnA98k= github.com/prometheus/common v0.63.0/go.mod h1:VVFF/fBIoToEnWRVkYoXEkq3R3paCoxG9PXP74SnV18= github.com/prometheus/procfs v0.16.0 h1:xh6oHhKwnOJKMYiYBDWmkHqQPyiY40sny36Cmx2bbsM= @@ -387,6 +394,8 @@ golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPh golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34= golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc= +golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE= +golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc= golang.org/x/exp v0.0.0-20250210185358-939b2ce775ac h1:l5+whBCLH3iH2ZNHYLbAe58bo7yrN4mVcnkHDYz5vvs= golang.org/x/exp v0.0.0-20250210185358-939b2ce775ac/go.mod h1:hH+7mtFmImwwcMvScyxUhjuVHR3HGaDPMn9rMSUUbxo= golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= @@ -406,6 +415,8 @@ golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qx golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8= golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= +golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY= +golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -456,8 +467,12 @@ golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8T golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/genproto/googleapis/api v0.0.0-20250404141209-ee84b53bf3d0 h1:Qbb5RVn5xzI4naMJSpJ7lhvmos6UwZkbekd5Uz7rt9E= google.golang.org/genproto/googleapis/api v0.0.0-20250404141209-ee84b53bf3d0/go.mod h1:6T35kB3IPpdw7Wul09by0G/JuOuIFkXV6OOvt8IZeT8= +google.golang.org/genproto/googleapis/api v0.0.0-20250409194420-de1ac958c67a h1:OQ7sHVzkx6L57dQpzUS4ckfWJ51KDH74XHTDe23xWAs= +google.golang.org/genproto/googleapis/api v0.0.0-20250409194420-de1ac958c67a/go.mod h1:2R6XrVC8Oc08GlNh8ujEpc7HkLiEZ16QeY7FxIs20ac= google.golang.org/genproto/googleapis/rpc v0.0.0-20250404141209-ee84b53bf3d0 h1:0K7wTWyzxZ7J+L47+LbFogJW1nn/gnnMCN0vGXNYtTI= google.golang.org/genproto/googleapis/rpc v0.0.0-20250404141209-ee84b53bf3d0/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250409194420-de1ac958c67a h1:GIqLhp/cYUkuGuiT+vJk8vhOP86L4+SP5j8yXgeVpvI= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250409194420-de1ac958c67a/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= google.golang.org/grpc v1.71.1 h1:ffsFWr7ygTUscGPI0KKK6TLrGz0476KUvvsbqWK0rPI= google.golang.org/grpc v1.71.1/go.mod h1:H0GRtasmQOh9LkFoCPDu3ZrwUtD1YGE+b2vYBYd/8Ec= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= diff --git a/go/pkg/db/api_find_by_id.sql_generated.go b/go/pkg/db/api_find_by_id.sql_generated.go new file mode 100644 index 0000000000..a8c87eaf93 --- /dev/null +++ b/go/pkg/db/api_find_by_id.sql_generated.go @@ -0,0 +1,35 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.27.0 +// source: api_find_by_id.sql + +package db + +import ( + "context" +) + +const findApiById = `-- name: FindApiById :one +SELECT id, name, workspace_id, ip_whitelist, auth_type, key_auth_id, created_at_m, updated_at_m, deleted_at_m, delete_protection FROM apis WHERE id = ? +` + +// FindApiById +// +// SELECT id, name, workspace_id, ip_whitelist, auth_type, key_auth_id, created_at_m, updated_at_m, deleted_at_m, delete_protection FROM apis WHERE id = ? +func (q *Queries) FindApiById(ctx context.Context, db DBTX, id string) (Api, error) { + row := db.QueryRowContext(ctx, findApiById, id) + var i Api + err := row.Scan( + &i.ID, + &i.Name, + &i.WorkspaceID, + &i.IpWhitelist, + &i.AuthType, + &i.KeyAuthID, + &i.CreatedAtM, + &i.UpdatedAtM, + &i.DeletedAtM, + &i.DeleteProtection, + ) + return i, err +} diff --git a/go/pkg/db/api_insert.sql_generated.go b/go/pkg/db/api_insert.sql_generated.go new file mode 100644 index 0000000000..a71d448966 --- /dev/null +++ b/go/pkg/db/api_insert.sql_generated.go @@ -0,0 +1,71 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.27.0 +// source: api_insert.sql + +package db + +import ( + "context" + "database/sql" +) + +const insertApi = `-- name: InsertApi :exec +INSERT INTO apis ( + id, + name, + workspace_id, + auth_type, + key_auth_id, + created_at_m, + deleted_at_m +) VALUES ( + ?, + ?, + ?, + ?, + ?, + ?, + NULL +) +` + +type InsertApiParams struct { + ID string `db:"id"` + Name string `db:"name"` + WorkspaceID string `db:"workspace_id"` + AuthType NullApisAuthType `db:"auth_type"` + KeyAuthID sql.NullString `db:"key_auth_id"` + CreatedAtM int64 `db:"created_at_m"` +} + +// InsertApi +// +// INSERT INTO apis ( +// id, +// name, +// workspace_id, +// auth_type, +// key_auth_id, +// created_at_m, +// deleted_at_m +// ) VALUES ( +// ?, +// ?, +// ?, +// ?, +// ?, +// ?, +// NULL +// ) +func (q *Queries) InsertApi(ctx context.Context, db DBTX, arg InsertApiParams) error { + _, err := db.ExecContext(ctx, insertApi, + arg.ID, + arg.Name, + arg.WorkspaceID, + arg.AuthType, + arg.KeyAuthID, + arg.CreatedAtM, + ) + return err +} diff --git a/go/pkg/db/audit_log_find_target_by_id.sql_generated.go b/go/pkg/db/audit_log_find_target_by_id.sql_generated.go new file mode 100644 index 0000000000..7d38ff42d8 --- /dev/null +++ b/go/pkg/db/audit_log_find_target_by_id.sql_generated.go @@ -0,0 +1,78 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.27.0 +// source: audit_log_find_target_by_id.sql + +package db + +import ( + "context" +) + +const findAuditLogTargetById = `-- name: FindAuditLogTargetById :many +SELECT audit_log_target.workspace_id, audit_log_target.bucket_id, audit_log_target.bucket, audit_log_target.audit_log_id, audit_log_target.display_name, audit_log_target.type, audit_log_target.id, audit_log_target.name, audit_log_target.meta, audit_log_target.created_at, audit_log_target.updated_at, audit_log.id, audit_log.workspace_id, audit_log.bucket, audit_log.bucket_id, audit_log.event, audit_log.time, audit_log.display, audit_log.remote_ip, audit_log.user_agent, audit_log.actor_type, audit_log.actor_id, audit_log.actor_name, audit_log.actor_meta, audit_log.created_at, audit_log.updated_at +FROM audit_log_target +JOIN audit_log ON audit_log.id = audit_log_target.audit_log_id +WHERE audit_log_target.id = ? +` + +type FindAuditLogTargetByIdRow struct { + AuditLogTarget AuditLogTarget `db:"audit_log_target"` + AuditLog AuditLog `db:"audit_log"` +} + +// FindAuditLogTargetById +// +// SELECT audit_log_target.workspace_id, audit_log_target.bucket_id, audit_log_target.bucket, audit_log_target.audit_log_id, audit_log_target.display_name, audit_log_target.type, audit_log_target.id, audit_log_target.name, audit_log_target.meta, audit_log_target.created_at, audit_log_target.updated_at, audit_log.id, audit_log.workspace_id, audit_log.bucket, audit_log.bucket_id, audit_log.event, audit_log.time, audit_log.display, audit_log.remote_ip, audit_log.user_agent, audit_log.actor_type, audit_log.actor_id, audit_log.actor_name, audit_log.actor_meta, audit_log.created_at, audit_log.updated_at +// FROM audit_log_target +// JOIN audit_log ON audit_log.id = audit_log_target.audit_log_id +// WHERE audit_log_target.id = ? +func (q *Queries) FindAuditLogTargetById(ctx context.Context, db DBTX, id string) ([]FindAuditLogTargetByIdRow, error) { + rows, err := db.QueryContext(ctx, findAuditLogTargetById, id) + if err != nil { + return nil, err + } + defer rows.Close() + var items []FindAuditLogTargetByIdRow + for rows.Next() { + var i FindAuditLogTargetByIdRow + if err := rows.Scan( + &i.AuditLogTarget.WorkspaceID, + &i.AuditLogTarget.BucketID, + &i.AuditLogTarget.Bucket, + &i.AuditLogTarget.AuditLogID, + &i.AuditLogTarget.DisplayName, + &i.AuditLogTarget.Type, + &i.AuditLogTarget.ID, + &i.AuditLogTarget.Name, + &i.AuditLogTarget.Meta, + &i.AuditLogTarget.CreatedAt, + &i.AuditLogTarget.UpdatedAt, + &i.AuditLog.ID, + &i.AuditLog.WorkspaceID, + &i.AuditLog.Bucket, + &i.AuditLog.BucketID, + &i.AuditLog.Event, + &i.AuditLog.Time, + &i.AuditLog.Display, + &i.AuditLog.RemoteIp, + &i.AuditLog.UserAgent, + &i.AuditLog.ActorType, + &i.AuditLog.ActorID, + &i.AuditLog.ActorName, + &i.AuditLog.ActorMeta, + &i.AuditLog.CreatedAt, + &i.AuditLog.UpdatedAt, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} diff --git a/go/pkg/db/models_generated.go b/go/pkg/db/models_generated.go index 987eb58547..504cd6f355 100644 --- a/go/pkg/db/models_generated.go +++ b/go/pkg/db/models_generated.go @@ -222,47 +222,6 @@ func (ns NullWorkspacesPlan) Value() (driver.Value, error) { return string(ns.WorkspacesPlan), nil } -type WorkspacesPlanDowngradeRequest string - -const ( - WorkspacesPlanDowngradeRequestFree WorkspacesPlanDowngradeRequest = "free" -) - -func (e *WorkspacesPlanDowngradeRequest) Scan(src interface{}) error { - switch s := src.(type) { - case []byte: - *e = WorkspacesPlanDowngradeRequest(s) - case string: - *e = WorkspacesPlanDowngradeRequest(s) - default: - return fmt.Errorf("unsupported scan type for WorkspacesPlanDowngradeRequest: %T", src) - } - return nil -} - -type NullWorkspacesPlanDowngradeRequest struct { - WorkspacesPlanDowngradeRequest WorkspacesPlanDowngradeRequest - Valid bool // Valid is true if WorkspacesPlanDowngradeRequest is not NULL -} - -// Scan implements the Scanner interface. -func (ns *NullWorkspacesPlanDowngradeRequest) Scan(value interface{}) error { - if value == nil { - ns.WorkspacesPlanDowngradeRequest, ns.Valid = "", false - return nil - } - ns.Valid = true - return ns.WorkspacesPlanDowngradeRequest.Scan(value) -} - -// Value implements the driver Valuer interface. -func (ns NullWorkspacesPlanDowngradeRequest) Value() (driver.Value, error) { - if !ns.Valid { - return nil, nil - } - return string(ns.WorkspacesPlanDowngradeRequest), nil -} - type Api struct { ID string `db:"id"` Name string `db:"name"` @@ -279,8 +238,8 @@ type Api struct { type AuditLog struct { ID string `db:"id"` WorkspaceID string `db:"workspace_id"` - BucketID string `db:"bucket_id"` Bucket string `db:"bucket"` + BucketID string `db:"bucket_id"` Event string `db:"event"` Time int64 `db:"time"` Display string `db:"display"` @@ -294,11 +253,21 @@ type AuditLog struct { UpdatedAt sql.NullInt64 `db:"updated_at"` } +type AuditLogBucket struct { + ID string `db:"id"` + WorkspaceID string `db:"workspace_id"` + Name string `db:"name"` + RetentionDays sql.NullInt32 `db:"retention_days"` + CreatedAt int64 `db:"created_at"` + UpdatedAt sql.NullInt64 `db:"updated_at"` + DeleteProtection sql.NullBool `db:"delete_protection"` +} + type AuditLogTarget struct { WorkspaceID string `db:"workspace_id"` BucketID string `db:"bucket_id"` - AuditLogID string `db:"audit_log_id"` Bucket string `db:"bucket"` + AuditLogID string `db:"audit_log_id"` DisplayName string `db:"display_name"` Type string `db:"type"` ID string `db:"id"` @@ -486,23 +455,19 @@ type VercelIntegration struct { } type Workspace struct { - ID string `db:"id"` - TenantID string `db:"tenant_id"` - OrgID sql.NullString `db:"org_id"` - Name string `db:"name"` - Plan NullWorkspacesPlan `db:"plan"` - Tier sql.NullString `db:"tier"` - StripeCustomerID sql.NullString `db:"stripe_customer_id"` - StripeSubscriptionID sql.NullString `db:"stripe_subscription_id"` - TrialEnds sql.NullTime `db:"trial_ends"` - BetaFeatures json.RawMessage `db:"beta_features"` - Features json.RawMessage `db:"features"` - PlanLockedUntil sql.NullTime `db:"plan_locked_until"` - PlanDowngradeRequest NullWorkspacesPlanDowngradeRequest `db:"plan_downgrade_request"` - PlanChanged sql.NullTime `db:"plan_changed"` - Enabled bool `db:"enabled"` - DeleteProtection sql.NullBool `db:"delete_protection"` - CreatedAtM int64 `db:"created_at_m"` - UpdatedAtM sql.NullInt64 `db:"updated_at_m"` - DeletedAtM sql.NullInt64 `db:"deleted_at_m"` + ID string `db:"id"` + OrgID string `db:"org_id"` + Name string `db:"name"` + Plan NullWorkspacesPlan `db:"plan"` + Tier sql.NullString `db:"tier"` + StripeCustomerID sql.NullString `db:"stripe_customer_id"` + StripeSubscriptionID sql.NullString `db:"stripe_subscription_id"` + BetaFeatures json.RawMessage `db:"beta_features"` + Features json.RawMessage `db:"features"` + Subscriptions []byte `db:"subscriptions"` + Enabled bool `db:"enabled"` + DeleteProtection sql.NullBool `db:"delete_protection"` + CreatedAtM int64 `db:"created_at_m"` + UpdatedAtM sql.NullInt64 `db:"updated_at_m"` + DeletedAtM sql.NullInt64 `db:"deleted_at_m"` } diff --git a/go/pkg/db/querier_generated.go b/go/pkg/db/querier_generated.go index 12bd54fce1..1d691146a6 100644 --- a/go/pkg/db/querier_generated.go +++ b/go/pkg/db/querier_generated.go @@ -16,6 +16,17 @@ type Querier interface { // SET deleted_at_m = ? // WHERE id = ? DeleteRatelimitNamespace(ctx context.Context, db DBTX, arg DeleteRatelimitNamespaceParams) (sql.Result, error) + //FindApiById + // + // SELECT id, name, workspace_id, ip_whitelist, auth_type, key_auth_id, created_at_m, updated_at_m, deleted_at_m, delete_protection FROM apis WHERE id = ? + FindApiById(ctx context.Context, db DBTX, id string) (Api, error) + //FindAuditLogTargetById + // + // SELECT audit_log_target.workspace_id, audit_log_target.bucket_id, audit_log_target.bucket, audit_log_target.audit_log_id, audit_log_target.display_name, audit_log_target.type, audit_log_target.id, audit_log_target.name, audit_log_target.meta, audit_log_target.created_at, audit_log_target.updated_at, audit_log.id, audit_log.workspace_id, audit_log.bucket, audit_log.bucket_id, audit_log.event, audit_log.time, audit_log.display, audit_log.remote_ip, audit_log.user_agent, audit_log.actor_type, audit_log.actor_id, audit_log.actor_name, audit_log.actor_meta, audit_log.created_at, audit_log.updated_at + // FROM audit_log_target + // JOIN audit_log ON audit_log.id = audit_log_target.audit_log_id + // WHERE audit_log_target.id = ? + FindAuditLogTargetById(ctx context.Context, db DBTX, id string) ([]FindAuditLogTargetByIdRow, error) //FindIdentityByID // // SELECT external_id, workspace_id, environment, meta, created_at, updated_at FROM identities WHERE id = ? @@ -164,7 +175,7 @@ type Querier interface { FindRatelimitsByIdentityID(ctx context.Context, db DBTX, identityID sql.NullString) ([]FindRatelimitsByIdentityIDRow, error) //FindWorkspaceByID // - // SELECT id, tenant_id, org_id, name, plan, tier, stripe_customer_id, stripe_subscription_id, trial_ends, beta_features, features, plan_locked_until, plan_downgrade_request, plan_changed, enabled, delete_protection, created_at_m, updated_at_m, deleted_at_m FROM `workspaces` + // SELECT id, org_id, name, plan, tier, stripe_customer_id, stripe_subscription_id, beta_features, features, subscriptions, enabled, delete_protection, created_at_m, updated_at_m, deleted_at_m FROM `workspaces` // WHERE id = ? FindWorkspaceByID(ctx context.Context, db DBTX, id string) (Workspace, error) //HardDeleteWorkspace @@ -173,6 +184,26 @@ type Querier interface { // WHERE id = ? // AND delete_protection = false HardDeleteWorkspace(ctx context.Context, db DBTX, id string) (sql.Result, error) + //InsertApi + // + // INSERT INTO apis ( + // id, + // name, + // workspace_id, + // auth_type, + // key_auth_id, + // created_at_m, + // deleted_at_m + // ) VALUES ( + // ?, + // ?, + // ?, + // ?, + // ?, + // ?, + // NULL + // ) + InsertApi(ctx context.Context, db DBTX, arg InsertApiParams) error //InsertAuditLog // // INSERT INTO `audit_log` ( @@ -415,10 +446,10 @@ type Querier interface { // // INSERT INTO `workspaces` ( // id, - // tenant_id, + // org_id, // name, // created_at_m, - // plan, + // tier, // beta_features, // features, // enabled, @@ -429,7 +460,7 @@ type Querier interface { // ?, // ?, // ?, - // 'free', + // 'Free', // '{}', // '{}', // true, @@ -439,7 +470,7 @@ type Querier interface { //ListWorkspaces // // SELECT - // w.id, w.tenant_id, w.org_id, w.name, w.plan, w.tier, w.stripe_customer_id, w.stripe_subscription_id, w.trial_ends, w.beta_features, w.features, w.plan_locked_until, w.plan_downgrade_request, w.plan_changed, w.enabled, w.delete_protection, w.created_at_m, w.updated_at_m, w.deleted_at_m, + // w.id, w.org_id, w.name, w.plan, w.tier, w.stripe_customer_id, w.stripe_subscription_id, w.beta_features, w.features, w.subscriptions, w.enabled, w.delete_protection, w.created_at_m, w.updated_at_m, w.deleted_at_m, // q.workspace_id, q.requests_per_month, q.logs_retention_days, q.audit_logs_retention_days, q.team // FROM `workspaces` w // LEFT JOIN quota q ON w.id = q.workspace_id diff --git a/go/pkg/db/queries/api_find_by_id.sql b/go/pkg/db/queries/api_find_by_id.sql new file mode 100644 index 0000000000..3986100355 --- /dev/null +++ b/go/pkg/db/queries/api_find_by_id.sql @@ -0,0 +1,2 @@ +-- name: FindApiById :one +SELECT * FROM apis WHERE id = ?; diff --git a/go/pkg/db/queries/api_insert.sql b/go/pkg/db/queries/api_insert.sql new file mode 100644 index 0000000000..a4c992d480 --- /dev/null +++ b/go/pkg/db/queries/api_insert.sql @@ -0,0 +1,18 @@ +-- name: InsertApi :exec +INSERT INTO apis ( + id, + name, + workspace_id, + auth_type, + key_auth_id, + created_at_m, + deleted_at_m +) VALUES ( + ?, + ?, + ?, + ?, + ?, + ?, + NULL +); diff --git a/go/pkg/db/queries/audit_log_find_target_by_id.sql b/go/pkg/db/queries/audit_log_find_target_by_id.sql new file mode 100644 index 0000000000..3ced47b925 --- /dev/null +++ b/go/pkg/db/queries/audit_log_find_target_by_id.sql @@ -0,0 +1,5 @@ +-- name: FindAuditLogTargetById :many +SELECT sqlc.embed(audit_log_target), sqlc.embed(audit_log) +FROM audit_log_target +JOIN audit_log ON audit_log.id = audit_log_target.audit_log_id +WHERE audit_log_target.id = sqlc.arg(id); diff --git a/go/pkg/db/queries/workspace_insert.sql b/go/pkg/db/queries/workspace_insert.sql index 7e71bf0068..4b7083176d 100644 --- a/go/pkg/db/queries/workspace_insert.sql +++ b/go/pkg/db/queries/workspace_insert.sql @@ -1,10 +1,10 @@ -- name: InsertWorkspace :exec INSERT INTO `workspaces` ( id, - tenant_id, + org_id, name, created_at_m, - plan, + tier, beta_features, features, enabled, @@ -12,10 +12,10 @@ INSERT INTO `workspaces` ( ) VALUES ( sqlc.arg(id), - sqlc.arg(tenant_id), + sqlc.arg(org_id), sqlc.arg(name), sqlc.arg(created_at), - 'free', + 'Free', '{}', '{}', true, diff --git a/go/pkg/db/schema.sql b/go/pkg/db/schema.sql index e3e2f33e3a..1b053d1453 100644 --- a/go/pkg/db/schema.sql +++ b/go/pkg/db/schema.sql @@ -176,26 +176,22 @@ CREATE TABLE `ratelimit_overrides` ( CREATE TABLE `workspaces` ( `id` varchar(256) NOT NULL, - `tenant_id` varchar(256) NOT NULL, - `org_id` varchar(256), + `org_id` varchar(256) NOT NULL, `name` varchar(256) NOT NULL, `plan` enum('free','pro','enterprise') DEFAULT 'free', `tier` varchar(256) DEFAULT 'Free', `stripe_customer_id` varchar(256), `stripe_subscription_id` varchar(256), - `trial_ends` datetime(3), `beta_features` json NOT NULL, `features` json NOT NULL, - `plan_locked_until` datetime(3), - `plan_downgrade_request` enum('free'), - `plan_changed` datetime(3), + `subscriptions` json, `enabled` boolean NOT NULL DEFAULT true, `delete_protection` boolean DEFAULT false, `created_at_m` bigint NOT NULL DEFAULT 0, `updated_at_m` bigint, `deleted_at_m` bigint, CONSTRAINT `workspaces_id` PRIMARY KEY(`id`), - CONSTRAINT `tenant_id_idx` UNIQUE(`tenant_id`) + CONSTRAINT `workspaces_org_id_unique` UNIQUE(`org_id`) ); CREATE TABLE `key_migration_errors` ( @@ -245,8 +241,8 @@ CREATE TABLE `quota` ( CREATE TABLE `audit_log` ( `id` varchar(256) NOT NULL, `workspace_id` varchar(256) NOT NULL, + `bucket` varchar(256) NOT NULL DEFAULT 'unkey_mutations', `bucket_id` varchar(256) NOT NULL, - `bucket` varchar(256) NOT NULL, `event` varchar(256) NOT NULL, `time` bigint NOT NULL, `display` varchar(256) NOT NULL, @@ -261,11 +257,23 @@ CREATE TABLE `audit_log` ( CONSTRAINT `audit_log_id` PRIMARY KEY(`id`) ); +CREATE TABLE `audit_log_bucket` ( + `id` varchar(256) NOT NULL, + `workspace_id` varchar(256) NOT NULL, + `name` varchar(256) NOT NULL, + `retention_days` int, + `created_at` bigint NOT NULL, + `updated_at` bigint, + `delete_protection` boolean DEFAULT false, + CONSTRAINT `audit_log_bucket_id` PRIMARY KEY(`id`), + CONSTRAINT `unique_name_per_workspace_idx` UNIQUE(`workspace_id`,`name`) +); + CREATE TABLE `audit_log_target` ( `workspace_id` varchar(256) NOT NULL, `bucket_id` varchar(256) NOT NULL, + `bucket` varchar(256) NOT NULL DEFAULT 'unkey_mutations', `audit_log_id` varchar(256) NOT NULL, - `bucket` varchar(256) NOT NULL, `display_name` varchar(256) NOT NULL, `type` varchar(256) NOT NULL, `id` varchar(256) NOT NULL, @@ -290,8 +298,10 @@ CREATE INDEX `identity_id_idx` ON `ratelimits` (`identity_id`); CREATE INDEX `key_id_idx` ON `ratelimits` (`key_id`); CREATE INDEX `workspace_id_idx` ON `audit_log` (`workspace_id`); CREATE INDEX `bucket_id_idx` ON `audit_log` (`bucket_id`); +CREATE INDEX `bucket_idx` ON `audit_log` (`bucket`); CREATE INDEX `event_idx` ON `audit_log` (`event`); CREATE INDEX `actor_id_idx` ON `audit_log` (`actor_id`); CREATE INDEX `time_idx` ON `audit_log` (`time`); +CREATE INDEX `bucket` ON `audit_log_target` (`bucket`); CREATE INDEX `audit_log_id` ON `audit_log_target` (`audit_log_id`); CREATE INDEX `id_idx` ON `audit_log_target` (`id`); diff --git a/go/pkg/db/workspace_find_by_id.sql_generated.go b/go/pkg/db/workspace_find_by_id.sql_generated.go index f7b87d6030..e24cc463ed 100644 --- a/go/pkg/db/workspace_find_by_id.sql_generated.go +++ b/go/pkg/db/workspace_find_by_id.sql_generated.go @@ -10,32 +10,28 @@ import ( ) const findWorkspaceByID = `-- name: FindWorkspaceByID :one -SELECT id, tenant_id, org_id, name, plan, tier, stripe_customer_id, stripe_subscription_id, trial_ends, beta_features, features, plan_locked_until, plan_downgrade_request, plan_changed, enabled, delete_protection, created_at_m, updated_at_m, deleted_at_m FROM ` + "`" + `workspaces` + "`" + ` +SELECT id, org_id, name, plan, tier, stripe_customer_id, stripe_subscription_id, beta_features, features, subscriptions, enabled, delete_protection, created_at_m, updated_at_m, deleted_at_m FROM ` + "`" + `workspaces` + "`" + ` WHERE id = ? ` // FindWorkspaceByID // -// SELECT id, tenant_id, org_id, name, plan, tier, stripe_customer_id, stripe_subscription_id, trial_ends, beta_features, features, plan_locked_until, plan_downgrade_request, plan_changed, enabled, delete_protection, created_at_m, updated_at_m, deleted_at_m FROM `workspaces` +// SELECT id, org_id, name, plan, tier, stripe_customer_id, stripe_subscription_id, beta_features, features, subscriptions, enabled, delete_protection, created_at_m, updated_at_m, deleted_at_m FROM `workspaces` // WHERE id = ? func (q *Queries) FindWorkspaceByID(ctx context.Context, db DBTX, id string) (Workspace, error) { row := db.QueryRowContext(ctx, findWorkspaceByID, id) var i Workspace err := row.Scan( &i.ID, - &i.TenantID, &i.OrgID, &i.Name, &i.Plan, &i.Tier, &i.StripeCustomerID, &i.StripeSubscriptionID, - &i.TrialEnds, &i.BetaFeatures, &i.Features, - &i.PlanLockedUntil, - &i.PlanDowngradeRequest, - &i.PlanChanged, + &i.Subscriptions, &i.Enabled, &i.DeleteProtection, &i.CreatedAtM, diff --git a/go/pkg/db/workspace_insert.sql_generated.go b/go/pkg/db/workspace_insert.sql_generated.go index 747fcec0ca..8967b87fc5 100644 --- a/go/pkg/db/workspace_insert.sql_generated.go +++ b/go/pkg/db/workspace_insert.sql_generated.go @@ -12,10 +12,10 @@ import ( const insertWorkspace = `-- name: InsertWorkspace :exec INSERT INTO ` + "`" + `workspaces` + "`" + ` ( id, - tenant_id, + org_id, name, created_at_m, - plan, + tier, beta_features, features, enabled, @@ -26,7 +26,7 @@ VALUES ( ?, ?, ?, - 'free', + 'Free', '{}', '{}', true, @@ -36,7 +36,7 @@ VALUES ( type InsertWorkspaceParams struct { ID string `db:"id"` - TenantID string `db:"tenant_id"` + OrgID string `db:"org_id"` Name string `db:"name"` CreatedAt int64 `db:"created_at"` } @@ -45,10 +45,10 @@ type InsertWorkspaceParams struct { // // INSERT INTO `workspaces` ( // id, -// tenant_id, +// org_id, // name, // created_at_m, -// plan, +// tier, // beta_features, // features, // enabled, @@ -59,7 +59,7 @@ type InsertWorkspaceParams struct { // ?, // ?, // ?, -// 'free', +// 'Free', // '{}', // '{}', // true, @@ -68,7 +68,7 @@ type InsertWorkspaceParams struct { func (q *Queries) InsertWorkspace(ctx context.Context, db DBTX, arg InsertWorkspaceParams) error { _, err := db.ExecContext(ctx, insertWorkspace, arg.ID, - arg.TenantID, + arg.OrgID, arg.Name, arg.CreatedAt, ) diff --git a/go/pkg/db/workspaces_list.sql_generated.go b/go/pkg/db/workspaces_list.sql_generated.go index c2cf2dc468..b281597760 100644 --- a/go/pkg/db/workspaces_list.sql_generated.go +++ b/go/pkg/db/workspaces_list.sql_generated.go @@ -11,7 +11,7 @@ import ( const listWorkspaces = `-- name: ListWorkspaces :many SELECT - w.id, w.tenant_id, w.org_id, w.name, w.plan, w.tier, w.stripe_customer_id, w.stripe_subscription_id, w.trial_ends, w.beta_features, w.features, w.plan_locked_until, w.plan_downgrade_request, w.plan_changed, w.enabled, w.delete_protection, w.created_at_m, w.updated_at_m, w.deleted_at_m, + w.id, w.org_id, w.name, w.plan, w.tier, w.stripe_customer_id, w.stripe_subscription_id, w.beta_features, w.features, w.subscriptions, w.enabled, w.delete_protection, w.created_at_m, w.updated_at_m, w.deleted_at_m, q.workspace_id, q.requests_per_month, q.logs_retention_days, q.audit_logs_retention_days, q.team FROM ` + "`" + `workspaces` + "`" + ` w LEFT JOIN quota q ON w.id = q.workspace_id @@ -28,7 +28,7 @@ type ListWorkspacesRow struct { // ListWorkspaces // // SELECT -// w.id, w.tenant_id, w.org_id, w.name, w.plan, w.tier, w.stripe_customer_id, w.stripe_subscription_id, w.trial_ends, w.beta_features, w.features, w.plan_locked_until, w.plan_downgrade_request, w.plan_changed, w.enabled, w.delete_protection, w.created_at_m, w.updated_at_m, w.deleted_at_m, +// w.id, w.org_id, w.name, w.plan, w.tier, w.stripe_customer_id, w.stripe_subscription_id, w.beta_features, w.features, w.subscriptions, w.enabled, w.delete_protection, w.created_at_m, w.updated_at_m, w.deleted_at_m, // q.workspace_id, q.requests_per_month, q.logs_retention_days, q.audit_logs_retention_days, q.team // FROM `workspaces` w // LEFT JOIN quota q ON w.id = q.workspace_id @@ -46,19 +46,15 @@ func (q *Queries) ListWorkspaces(ctx context.Context, db DBTX, cursor string) ([ var i ListWorkspacesRow if err := rows.Scan( &i.Workspace.ID, - &i.Workspace.TenantID, &i.Workspace.OrgID, &i.Workspace.Name, &i.Workspace.Plan, &i.Workspace.Tier, &i.Workspace.StripeCustomerID, &i.Workspace.StripeSubscriptionID, - &i.Workspace.TrialEnds, &i.Workspace.BetaFeatures, &i.Workspace.Features, - &i.Workspace.PlanLockedUntil, - &i.Workspace.PlanDowngradeRequest, - &i.Workspace.PlanChanged, + &i.Workspace.Subscriptions, &i.Workspace.Enabled, &i.Workspace.DeleteProtection, &i.Workspace.CreatedAtM, diff --git a/go/pkg/testutil/seed/seed.go b/go/pkg/testutil/seed/seed.go index 18d4339b9b..0722c0c44b 100644 --- a/go/pkg/testutil/seed/seed.go +++ b/go/pkg/testutil/seed/seed.go @@ -44,7 +44,7 @@ func (s *Seeder) Seed(ctx context.Context) { // Insert root workspace insertRootWorkspaceParams := db.InsertWorkspaceParams{ ID: uid.New("test_ws"), - TenantID: uid.New("unkey"), + OrgID: uid.New("unkey"), Name: "unkey", CreatedAt: time.Now().UnixMilli(), } @@ -73,7 +73,7 @@ func (s *Seeder) Seed(ctx context.Context) { // Insert user workspace insertUserWorkspaceParams := db.InsertWorkspaceParams{ ID: uid.New("test_ws"), - TenantID: uid.New("user"), + OrgID: uid.New("user"), Name: "user", CreatedAt: time.Now().UnixMilli(), } @@ -87,7 +87,7 @@ func (s *Seeder) Seed(ctx context.Context) { // Insert different workspace for permission tests insertDifferentWorkspaceParams := db.InsertWorkspaceParams{ ID: uid.New("test_ws"), - TenantID: uid.New("alice"), + OrgID: uid.New("alice"), Name: "alice", CreatedAt: time.Now().UnixMilli(), } @@ -127,6 +127,7 @@ func (s *Seeder) CreateRootKey(ctx context.Context, workspaceID string, permissi if len(permissions) > 0 { for _, permission := range permissions { + s.t.Logf("creating permission %s for key %s", permission, insertKeyParams.ID) permissionID := uid.New(uid.TestPrefix) err := db.Query.InsertPermission(ctx, s.DB.RW(), db.InsertPermissionParams{ ID: permissionID, @@ -139,15 +140,13 @@ func (s *Seeder) CreateRootKey(ctx context.Context, workspaceID string, permissi mysqlErr := &mysql.MySQLError{} // nolint:exhaustruct if errors.As(err, &mysqlErr) { // Error 1062 (23000): Duplicate entry - if mysqlErr.Number == 1064 { - existing, findErr := db.Query.FindPermissionByWorkspaceAndName(ctx, s.DB.RO(), db.FindPermissionByWorkspaceAndNameParams{ - WorkspaceID: s.Resources.RootWorkspace.ID, - Name: permission, - }) - require.NoError(s.t, findErr) - s.t.Logf("found existing permission: %+v", existing) - permissionID = existing.ID - } + require.Equal(s.t, uint16(1062), mysqlErr.Number) + existing, findErr := db.Query.FindPermissionByWorkspaceAndName(ctx, s.DB.RO(), db.FindPermissionByWorkspaceAndNameParams{ + WorkspaceID: s.Resources.RootWorkspace.ID, + Name: permission, + }) + require.NoError(s.t, findErr) + permissionID = existing.ID } else { require.NoError(s.t, err) diff --git a/internal/db/drizzle/0000_cool_kulan_gath.sql b/internal/db/drizzle/0000_motionless_vargas.sql similarity index 96% rename from internal/db/drizzle/0000_cool_kulan_gath.sql rename to internal/db/drizzle/0000_motionless_vargas.sql index c483eaa478..56f4de0fa5 100644 --- a/internal/db/drizzle/0000_cool_kulan_gath.sql +++ b/internal/db/drizzle/0000_motionless_vargas.sql @@ -176,19 +176,14 @@ CREATE TABLE `ratelimit_overrides` ( --> statement-breakpoint CREATE TABLE `workspaces` ( `id` varchar(256) NOT NULL, - `tenant_id` varchar(256) NOT NULL, - `org_id` varchar(256), + `org_id` varchar(256) NOT NULL, `name` varchar(256) NOT NULL, `plan` enum('free','pro','enterprise') DEFAULT 'free', `tier` varchar(256) DEFAULT 'Free', `stripe_customer_id` varchar(256), `stripe_subscription_id` varchar(256), - `trial_ends` datetime(3), `beta_features` json NOT NULL, `features` json NOT NULL, - `plan_locked_until` datetime(3), - `plan_downgrade_request` enum('free'), - `plan_changed` datetime(3), `subscriptions` json, `enabled` boolean NOT NULL DEFAULT true, `delete_protection` boolean DEFAULT false, @@ -196,7 +191,7 @@ CREATE TABLE `workspaces` ( `updated_at_m` bigint, `deleted_at_m` bigint, CONSTRAINT `workspaces_id` PRIMARY KEY(`id`), - CONSTRAINT `tenant_id_idx` UNIQUE(`tenant_id`) + CONSTRAINT `workspaces_org_id_unique` UNIQUE(`org_id`) ); --> statement-breakpoint CREATE TABLE `key_migration_errors` ( @@ -246,6 +241,7 @@ CREATE TABLE `quota` ( CREATE TABLE `audit_log` ( `id` varchar(256) NOT NULL, `workspace_id` varchar(256) NOT NULL, + `bucket` varchar(256) NOT NULL DEFAULT 'unkey_mutations', `bucket_id` varchar(256) NOT NULL, `event` varchar(256) NOT NULL, `time` bigint NOT NULL, @@ -276,6 +272,7 @@ CREATE TABLE `audit_log_bucket` ( CREATE TABLE `audit_log_target` ( `workspace_id` varchar(256) NOT NULL, `bucket_id` varchar(256) NOT NULL, + `bucket` varchar(256) NOT NULL DEFAULT 'unkey_mutations', `audit_log_id` varchar(256) NOT NULL, `display_name` varchar(256) NOT NULL, `type` varchar(256) NOT NULL, @@ -301,8 +298,10 @@ CREATE INDEX `identity_id_idx` ON `ratelimits` (`identity_id`);--> statement-bre CREATE INDEX `key_id_idx` ON `ratelimits` (`key_id`);--> statement-breakpoint CREATE INDEX `workspace_id_idx` ON `audit_log` (`workspace_id`);--> statement-breakpoint CREATE INDEX `bucket_id_idx` ON `audit_log` (`bucket_id`);--> statement-breakpoint +CREATE INDEX `bucket_idx` ON `audit_log` (`bucket`);--> statement-breakpoint CREATE INDEX `event_idx` ON `audit_log` (`event`);--> statement-breakpoint CREATE INDEX `actor_id_idx` ON `audit_log` (`actor_id`);--> statement-breakpoint CREATE INDEX `time_idx` ON `audit_log` (`time`);--> statement-breakpoint +CREATE INDEX `bucket` ON `audit_log_target` (`bucket`);--> statement-breakpoint CREATE INDEX `audit_log_id` ON `audit_log_target` (`audit_log_id`);--> statement-breakpoint CREATE INDEX `id_idx` ON `audit_log_target` (`id`); \ No newline at end of file diff --git a/internal/db/drizzle/meta/0000_snapshot.json b/internal/db/drizzle/meta/0000_snapshot.json index 6ab030c09c..59d8049634 100644 --- a/internal/db/drizzle/meta/0000_snapshot.json +++ b/internal/db/drizzle/meta/0000_snapshot.json @@ -1,7 +1,7 @@ { "version": "5", "dialect": "mysql", - "id": "ee1b848f-4a6b-4f2d-ae13-08fb6292a7a2", + "id": "b652299c-35af-4024-84c7-13f686c5195d", "prevId": "00000000-0000-0000-0000-000000000000", "tables": { "apis": { @@ -1107,18 +1107,11 @@ "notNull": true, "autoincrement": false }, - "tenant_id": { - "name": "tenant_id", - "type": "varchar(256)", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, "org_id": { "name": "org_id", "type": "varchar(256)", "primaryKey": false, - "notNull": false, + "notNull": true, "autoincrement": false }, "name": { @@ -1158,13 +1151,6 @@ "notNull": false, "autoincrement": false }, - "trial_ends": { - "name": "trial_ends", - "type": "datetime(3)", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, "beta_features": { "name": "beta_features", "type": "json", @@ -1179,27 +1165,6 @@ "notNull": true, "autoincrement": false }, - "plan_locked_until": { - "name": "plan_locked_until", - "type": "datetime(3)", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "plan_downgrade_request": { - "name": "plan_downgrade_request", - "type": "enum('free')", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "plan_changed": { - "name": "plan_changed", - "type": "datetime(3)", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, "subscriptions": { "name": "subscriptions", "type": "json", @@ -1246,13 +1211,7 @@ "autoincrement": false } }, - "indexes": { - "tenant_id_idx": { - "name": "tenant_id_idx", - "columns": ["tenant_id"], - "isUnique": true - } - }, + "indexes": {}, "foreignKeys": {}, "compositePrimaryKeys": { "workspaces_id": { @@ -1260,7 +1219,12 @@ "columns": ["id"] } }, - "uniqueConstraints": {} + "uniqueConstraints": { + "workspaces_org_id_unique": { + "name": "workspaces_org_id_unique", + "columns": ["org_id"] + } + } }, "key_migration_errors": { "name": "key_migration_errors", @@ -1554,6 +1518,14 @@ "notNull": true, "autoincrement": false }, + "bucket": { + "name": "bucket", + "type": "varchar(256)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'unkey_mutations'" + }, "bucket_id": { "name": "bucket_id", "type": "varchar(256)", @@ -1650,6 +1622,11 @@ "columns": ["bucket_id"], "isUnique": false }, + "bucket_idx": { + "name": "bucket_idx", + "columns": ["bucket"], + "isUnique": false + }, "event_idx": { "name": "event_idx", "columns": ["event"], @@ -1762,6 +1739,14 @@ "notNull": true, "autoincrement": false }, + "bucket": { + "name": "bucket", + "type": "varchar(256)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'unkey_mutations'" + }, "audit_log_id": { "name": "audit_log_id", "type": "varchar(256)", @@ -1820,6 +1805,11 @@ } }, "indexes": { + "bucket": { + "name": "bucket", + "columns": ["bucket"], + "isUnique": false + }, "audit_log_id": { "name": "audit_log_id", "columns": ["audit_log_id"], diff --git a/internal/db/drizzle/meta/_journal.json b/internal/db/drizzle/meta/_journal.json index 0e727c6c20..633273e231 100644 --- a/internal/db/drizzle/meta/_journal.json +++ b/internal/db/drizzle/meta/_journal.json @@ -5,8 +5,8 @@ { "idx": 0, "version": "5", - "when": 1741427042118, - "tag": "0000_cool_kulan_gath", + "when": 1744529464325, + "tag": "0000_motionless_vargas", "breakpoints": true } ]