From f246df66ee8f656334da122541ed0fcbc97ed576 Mon Sep 17 00:00:00 2001 From: Derek Higgins Date: Tue, 24 Mar 2026 17:01:35 +0000 Subject: [PATCH] Fix workspace name uniqueness to be scoped per user Previously workspace names were globally unique across all users, preventing users from creating workspaces with names already taken by others (e.g., 'ambient-code'). Changes: - Add owner_user_id field to Project model - Change unique constraint from name-only to composite (owner_user_id, name) - Automatically set owner_user_id from authenticated user context - Add migration to update existing database schema - Update OpenAPI schema to include owner_user_id field - Fix ID generation to use unique IDs instead of name This allows each user to have their own workspaces with any name, with uniqueness scoped only within their own workspace collection. --- .../openapi/openapi.projects.yaml | 4 ++ .../plugins/projects/handler.go | 4 ++ .../plugins/projects/migration.go | 39 +++++++++++++++++++ .../plugins/projects/model.go | 5 ++- .../plugins/projects/plugin.go | 1 + 5 files changed, 51 insertions(+), 2 deletions(-) diff --git a/components/ambient-api-server/openapi/openapi.projects.yaml b/components/ambient-api-server/openapi/openapi.projects.yaml index 2f2680c2a..cd92b7ca2 100644 --- a/components/ambient-api-server/openapi/openapi.projects.yaml +++ b/components/ambient-api-server/openapi/openapi.projects.yaml @@ -215,6 +215,10 @@ components: required: - name properties: + owner_user_id: + type: string + description: User ID of the project owner (set automatically by the server) + readOnly: true name: type: string display_name: diff --git a/components/ambient-api-server/plugins/projects/handler.go b/components/ambient-api-server/plugins/projects/handler.go index 702467698..b97d0120f 100644 --- a/components/ambient-api-server/plugins/projects/handler.go +++ b/components/ambient-api-server/plugins/projects/handler.go @@ -7,6 +7,7 @@ import ( "github.com/ambient-code/platform/components/ambient-api-server/pkg/api/openapi" "github.com/openshift-online/rh-trex-ai/pkg/api/presenters" + "github.com/openshift-online/rh-trex-ai/pkg/auth" "github.com/openshift-online/rh-trex-ai/pkg/errors" "github.com/openshift-online/rh-trex-ai/pkg/handlers" "github.com/openshift-online/rh-trex-ai/pkg/services" @@ -36,6 +37,9 @@ func (h projectHandler) Create(w http.ResponseWriter, r *http.Request) { Action: func() (interface{}, *errors.ServiceError) { ctx := r.Context() projectModel := ConvertProject(project) + if username := auth.GetUsernameFromContext(ctx); username != "" { + projectModel.OwnerUserId = username + } projectModel, err := h.project.Create(ctx, projectModel) if err != nil { return nil, err diff --git a/components/ambient-api-server/plugins/projects/migration.go b/components/ambient-api-server/plugins/projects/migration.go index c8b4ddaeb..f4621518b 100644 --- a/components/ambient-api-server/plugins/projects/migration.go +++ b/components/ambient-api-server/plugins/projects/migration.go @@ -28,3 +28,42 @@ func migration() *gormigrate.Migration { }, } } + +func ownerUserIdMigration() *gormigrate.Migration { + migrateStatements := []string{ + // Add owner_user_id column with a default empty string for existing rows + `ALTER TABLE projects ADD COLUMN IF NOT EXISTS owner_user_id TEXT NOT NULL DEFAULT ''`, + // Drop the old unique index on name only + `DROP INDEX IF EXISTS idx_projects_name`, + // Create composite unique index on (owner_user_id, name) + `CREATE UNIQUE INDEX IF NOT EXISTS idx_owner_name ON projects(owner_user_id, name)`, + } + rollbackStatements := []string{ + // Drop the composite unique index + `DROP INDEX IF EXISTS idx_owner_name`, + // Recreate the old unique index on name only + `CREATE UNIQUE INDEX IF NOT EXISTS idx_projects_name ON projects(name)`, + // Drop the owner_user_id column + `ALTER TABLE projects DROP COLUMN IF EXISTS owner_user_id`, + } + + return &gormigrate.Migration{ + ID: "202603240001", + Migrate: func(tx *gorm.DB) error { + for _, stmt := range migrateStatements { + if err := tx.Exec(stmt).Error; err != nil { + return err + } + } + return nil + }, + Rollback: func(tx *gorm.DB) error { + for _, stmt := range rollbackStatements { + if err := tx.Exec(stmt).Error; err != nil { + return err + } + } + return nil + }, + } +} diff --git a/components/ambient-api-server/plugins/projects/model.go b/components/ambient-api-server/plugins/projects/model.go index 175462b99..85a21a7b8 100644 --- a/components/ambient-api-server/plugins/projects/model.go +++ b/components/ambient-api-server/plugins/projects/model.go @@ -7,7 +7,8 @@ import ( type Project struct { api.Meta - Name string `json:"name" gorm:"uniqueIndex;not null"` + OwnerUserId string `json:"owner_user_id" gorm:"uniqueIndex:idx_owner_name;not null"` + Name string `json:"name" gorm:"uniqueIndex:idx_owner_name;not null"` DisplayName *string `json:"display_name"` Description *string `json:"description"` Labels *string `json:"labels"` @@ -27,7 +28,7 @@ func (l ProjectList) Index() ProjectIndex { } func (d *Project) BeforeCreate(tx *gorm.DB) error { - d.ID = d.Name + d.ID = api.NewID() return nil } diff --git a/components/ambient-api-server/plugins/projects/plugin.go b/components/ambient-api-server/plugins/projects/plugin.go index 8e7a249d2..7edb6da5d 100644 --- a/components/ambient-api-server/plugins/projects/plugin.go +++ b/components/ambient-api-server/plugins/projects/plugin.go @@ -101,4 +101,5 @@ func init() { }) db.RegisterMigration(migration()) + db.RegisterMigration(ownerUserIdMigration()) }