diff --git a/pkg/api/authn/noauth.go b/pkg/api/authn/noauth.go index 233f201a6..ec99e6246 100644 --- a/pkg/api/authn/noauth.go +++ b/pkg/api/authn/noauth.go @@ -27,6 +27,7 @@ func (n *NoAuth) AuthenticateRequest(req *http.Request) (*authenticator.Response req.Context(), &types.Identity{ ProviderUsername: "nobody", + ProviderUserID: "nobody", }, req.Header.Get("X-Obot-User-Timezone"), types2.RoleAdmin, diff --git a/pkg/bootstrap/bootstrap.go b/pkg/bootstrap/bootstrap.go index 69f2d8b39..b2972a8d1 100644 --- a/pkg/bootstrap/bootstrap.go +++ b/pkg/bootstrap/bootstrap.go @@ -152,6 +152,7 @@ func (b *Bootstrap) AuthenticateRequest(req *http.Request) (*authenticator.Respo req.Context(), &types.Identity{ ProviderUsername: "bootstrap", + ProviderUserID: "bootstrap", }, req.Header.Get("X-Obot-User-Timezone"), types2.RoleAdmin, diff --git a/pkg/gateway/client/auth.go b/pkg/gateway/client/auth.go index 3c73342a1..d7c1f2c4f 100644 --- a/pkg/gateway/client/auth.go +++ b/pkg/gateway/client/auth.go @@ -37,6 +37,7 @@ func (u UserDecorator) AuthenticateRequest(req *http.Request) (*authenticator.Re AuthProviderName: firstValue(resp.User.GetExtra(), "auth_provider_name"), AuthProviderNamespace: firstValue(resp.User.GetExtra(), "auth_provider_namespace"), ProviderUsername: resp.User.GetName(), + ProviderUserID: resp.User.GetUID(), }, req.Header.Get("X-Obot-User-Timezone")) if err != nil { return nil, false, err diff --git a/pkg/gateway/client/identity.go b/pkg/gateway/client/identity.go index 0289d728b..27c66b343 100644 --- a/pkg/gateway/client/identity.go +++ b/pkg/gateway/client/identity.go @@ -3,6 +3,7 @@ package client import ( "context" "errors" + "fmt" types2 "github.com/obot-platform/obot/apiclient/types" "github.com/obot-platform/obot/pkg/gateway/types" @@ -37,14 +38,39 @@ func (c *Client) EnsureIdentityWithRole(ctx context.Context, id *types.Identity, func ensureIdentity(tx *gorm.DB, id *types.Identity, timezone string, role types2.Role) (*types.User, error) { email := id.Email if err := tx.First(id).Error; errors.Is(err, gorm.ErrRecordNotFound) { - if err = tx.Create(id).Error; err != nil { + // Before we try creating a new identity, we need to check if there is one that has not been fully migrated yet. + migratedIdentity := &types.Identity{ + ProviderUsername: id.ProviderUsername, + ProviderUserID: fmt.Sprintf("OBOT_PLACEHOLDER_%s", id.ProviderUsername), + AuthProviderName: id.AuthProviderName, + AuthProviderNamespace: id.AuthProviderNamespace, + } + if err := tx.First(migratedIdentity).Error; errors.Is(err, gorm.ErrRecordNotFound) { + // If the identity does not exist, we can create it. + if err = tx.Create(id).Error; err != nil { + return nil, err + } + } else if err != nil { return nil, err + } else { + // The migrated identity exists. We need to update it with the right provider_user_id. + if err := tx.Model(&migratedIdentity).Where("provider_user_id = ?", fmt.Sprintf("OBOT_PLACEHOLDER_%s", id.ProviderUsername)).Update("provider_user_id", id.ProviderUserID).Error; err != nil { + return nil, err + } + + // Now we should be able to load the identity. + if err := tx.First(id).Error; err != nil { + return nil, err + } } } else if err != nil { return nil, err - } else if id.Email != email { + } + + // Check to see if the email got updated. + if id.Email != email { id.Email = email - if err = tx.Updates(id).Error; err != nil { + if err := tx.Updates(id).Error; err != nil { return nil, err } } diff --git a/pkg/gateway/db/db.go b/pkg/gateway/db/db.go index 82dd92752..a51a6a730 100644 --- a/pkg/gateway/db/db.go +++ b/pkg/gateway/db/db.go @@ -36,6 +36,57 @@ func (db *DB) AutoMigrate() (err error) { } }() + // Only run PostgreSQL-specific migrations if using PostgreSQL + if db.gormDB.Dialector.Name() == "postgres" { + // Check if the identities table exists + var exists bool + if err := tx.Raw(` + SELECT EXISTS ( + SELECT 1 + FROM information_schema.tables + WHERE table_name = 'identities' + ) + `).Scan(&exists).Error; err != nil { + return err + } + + if exists { + // The identities table needs to have auth_provider_namespace,auth_provider_name,provider_user_id as a primary key. + // It used to have auth_provider_namespace,auth_provider_name,provider_username as a primary key. + + // Check if the migration is needed. + var needsIdentityMigration bool + if err := tx.Raw(` + SELECT COUNT(*) = 0 as needs_migration + FROM information_schema.key_column_usage + WHERE table_name = 'identities' + AND constraint_name = 'identities_pkey' + AND column_name = 'provider_user_id' + `).Scan(&needsIdentityMigration).Error; err != nil { + return err + } + + if needsIdentityMigration { + // Add provider_user_id to identities table and update primary key. + if err := tx.Exec(` + -- Drop existing primary key + ALTER TABLE identities DROP CONSTRAINT identities_pkey; + + -- Add provider_user_id column + ALTER TABLE identities ADD COLUMN provider_user_id text NOT NULL DEFAULT ''; + + -- Set placeholder values for existing records + UPDATE identities SET provider_user_id = 'OBOT_PLACEHOLDER_' || provider_username WHERE provider_user_id = ''; + + -- Add new primary key + ALTER TABLE identities ADD PRIMARY KEY (auth_provider_name, auth_provider_namespace, provider_user_id); + `).Error; err != nil { + return err + } + } + } + } + return tx.AutoMigrate( types.AuthToken{}, types.TokenRequest{}, diff --git a/pkg/gateway/types/identity.go b/pkg/gateway/types/identity.go index d6ac32896..6264f8e17 100644 --- a/pkg/gateway/types/identity.go +++ b/pkg/gateway/types/identity.go @@ -5,7 +5,8 @@ import "time" type Identity struct { AuthProviderName string `json:"authProviderName" gorm:"primaryKey;index:idx_user_auth_id"` AuthProviderNamespace string `json:"authProviderNamespace" gorm:"primaryKey;index:idx_user_auth_id"` - ProviderUsername string `json:"providerUsername" gorm:"primaryKey"` + ProviderUsername string `json:"providerUsername"` + ProviderUserID string `json:"providerUserID" gorm:"primaryKey"` Email string `json:"email"` UserID uint `json:"userID" gorm:"index:idx_user_auth_id"` IconURL string `json:"iconURL"` diff --git a/pkg/services/config.go b/pkg/services/config.go index 5db4d12ae..ff644a806 100644 --- a/pkg/services/config.go +++ b/pkg/services/config.go @@ -364,6 +364,7 @@ func New(ctx context.Context, config Config) (*Services, error) { // is enabled. if err := gatewayClient.RemoveIdentity(ctx, &types.Identity{ ProviderUsername: "nobody", + ProviderUserID: "nobody", }); err != nil { return nil, fmt.Errorf(`failed to remove "nobody" user and identity from database: %w`, err) }