Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion backend/internal/huma/handlers/dashboard_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ func TestDashboardHandlerGetDashboardReturnsSnapshot(t *testing.T) {
Name: "expiring-soon",
KeyHash: "hash-soon",
KeyPrefix: "arc_test_handler",
UserID: "user-1",
UserID: new("user-1"),
ExpiresAt: new(time.Now().Add(12 * time.Hour)),
}).Error)

Expand Down
2 changes: 1 addition & 1 deletion backend/internal/models/api_key.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ type ApiKey struct {
KeyHash string `json:"-" gorm:"column:key_hash;not null"`
KeyPrefix string `json:"keyPrefix" gorm:"column:key_prefix;not null"`
ManagedBy *string `json:"-" gorm:"column:managed_by"`
UserID string `json:"userId" gorm:"column:user_id;not null"`
UserID *string `json:"userId,omitempty" gorm:"column:user_id"`
EnvironmentID *string `json:"environmentId,omitempty" gorm:"column:environment_id"`
ExpiresAt *time.Time `json:"expiresAt,omitempty" gorm:"column:expires_at" sortable:"true"`
LastUsedAt *time.Time `json:"lastUsedAt,omitempty" gorm:"column:last_used_at" sortable:"true"`
Expand Down
16 changes: 10 additions & 6 deletions backend/internal/services/api_key_service.go
Original file line number Diff line number Diff line change
Expand Up @@ -105,12 +105,12 @@ func (s *ApiKeyService) CreateApiKey(ctx context.Context, userID string, req api
return nil, err
}

return s.createAPIKeyWithRawKey(ctx, userID, rawKey, req, nil, nil)
return s.createAPIKeyWithRawKey(ctx, &userID, rawKey, req, nil, nil)
}

func (s *ApiKeyService) createAPIKeyWithRawKey(
ctx context.Context,
userID string,
userID *string,
rawKey string,
req apikey.CreateApiKey,
managedBy *string,
Expand Down Expand Up @@ -168,7 +168,7 @@ func toAPIKeyDTOInternal(ak *models.ApiKey) apikey.ApiKey {
}

func (s *ApiKeyService) CreateDefaultAdminAPIKey(ctx context.Context, userID, rawKey string) (*apikey.ApiKeyCreatedDto, error) {
return s.createAPIKeyWithRawKey(ctx, userID, rawKey, apikey.CreateApiKey{
return s.createAPIKeyWithRawKey(ctx, &userID, rawKey, apikey.CreateApiKey{
Name: defaultAdminAPIKeyName,
Description: defaultAdminAPIKeyDescription,
}, new(managedByAdminBootstrap), nil)
Expand Down Expand Up @@ -258,7 +258,7 @@ func (s *ApiKeyService) createManagedDefaultAdminAPIKey(tx *gorm.DB, userID, raw
KeyHash: keyHash,
KeyPrefix: keyPrefix,
ManagedBy: new(managedByAdminBootstrap),
UserID: userID,
UserID: &userID,
}

if err := tx.Create(ak).Error; err != nil {
Expand Down Expand Up @@ -316,7 +316,7 @@ func (s *ApiKeyService) CreateEnvironmentApiKey(ctx context.Context, environment
envIDShort = environmentID[:8]
}
name := fmt.Sprintf("Environment Bootstrap Key - %s", envIDShort)
return s.createAPIKeyWithRawKey(ctx, userID, rawKey, apikey.CreateApiKey{
return s.createAPIKeyWithRawKey(ctx, nil, rawKey, apikey.CreateApiKey{
Name: name,
Description: new("Auto-generated key for environment pairing"),
}, nil, &environmentID)
Expand Down Expand Up @@ -452,7 +452,11 @@ func (s *ApiKeyService) ValidateApiKey(ctx context.Context, rawKey string) (*mod

s.markApiKeyUsedAsync(ctx, apiKey.ID)

user, err := s.userService.GetUserByID(ctx, apiKey.UserID)
if apiKey.UserID == nil {
return nil, ErrApiKeyInvalid
}
Comment thread
kmendell marked this conversation as resolved.

user, err := s.userService.GetUserByID(ctx, *apiKey.UserID)
if err != nil {
return nil, fmt.Errorf("failed to get user for API key: %w", err)
}
Expand Down
12 changes: 6 additions & 6 deletions backend/internal/services/dashboard_service_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -90,28 +90,28 @@ func TestDashboardService_GetActionItems_IncludesExpiringAPIKeys(t *testing.T) {
Name: "expiring-soon",
KeyHash: "hash-soon",
KeyPrefix: "arc_test_s",
UserID: "user-1",
UserID: new("user-1"),
ExpiresAt: new(now.Add(24 * time.Hour)),
})
createDashboardTestAPIKey(t, db, models.ApiKey{
Name: "already-expired",
KeyHash: "hash-expired",
KeyPrefix: "arc_test_e",
UserID: "user-1",
UserID: new("user-1"),
ExpiresAt: new(now.Add(-24 * time.Hour)),
})
createDashboardTestAPIKey(t, db, models.ApiKey{
Name: "future",
KeyHash: "hash-future",
KeyPrefix: "arc_test_f",
UserID: "user-1",
UserID: new("user-1"),
ExpiresAt: new(now.Add(45 * 24 * time.Hour)),
})
createDashboardTestAPIKey(t, db, models.ApiKey{
Name: "never-expires",
KeyHash: "hash-never",
KeyPrefix: "arc_test_n",
UserID: "user-1",
UserID: new("user-1"),
})

actionItems, err := svc.GetActionItems(context.Background(), DashboardActionItemsOptions{})
Expand All @@ -133,7 +133,7 @@ func TestDashboardService_GetActionItems_DebugAllGoodReturnsNoItems(t *testing.T
Name: "expiring-soon",
KeyHash: "hash-soon",
KeyPrefix: "arc_test_d",
UserID: "user-1",
UserID: new("user-1"),
ExpiresAt: new(time.Now().Add(2 * time.Hour)),
})

Expand Down Expand Up @@ -194,7 +194,7 @@ func TestDashboardService_GetSnapshot_ReturnsDashboardSnapshot(t *testing.T) {
Name: "expiring-soon",
KeyHash: "hash-soon",
KeyPrefix: "arc_test_snapshot",
UserID: "user-1",
UserID: new("user-1"),
ExpiresAt: new(time.Now().Add(12 * time.Hour)),
})

Expand Down
2 changes: 1 addition & 1 deletion backend/internal/services/environment_service_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -533,7 +533,7 @@ func TestEnvironmentService_EnsureSwarmNodeAgentEnvironment_CreatesHiddenChildAn
Order("created_at asc").
Find(&apiKeys).Error)
require.Len(t, apiKeys, 1)
require.Equal(t, user.ID, apiKeys[0].UserID)
require.Nil(t, apiKeys[0].UserID) // environment bootstrap keys have no owner

reusedEnv, reusedToken, err := svc.EnsureSwarmNodeAgentEnvironment(
ctx,
Expand Down
2 changes: 2 additions & 0 deletions backend/pkg/utils/httpx/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ func NewHTTPClient() *http.Client {
IdleConnTimeout: 90 * time.Second,
TLSHandshakeTimeout: 5 * time.Second,
ExpectContinueTimeout: 1 * time.Second,
ForceAttemptHTTP2: true,
}
return &http.Client{
Transport: transport,
Expand All @@ -26,6 +27,7 @@ func NewHTTPClientWithTimeout(timeout time.Duration) *http.Client {
IdleConnTimeout: 90 * time.Second,
TLSHandshakeTimeout: 10 * time.Second,
ExpectContinueTimeout: 1 * time.Second,
ForceAttemptHTTP2: true,
}
return &http.Client{
Transport: transport,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
-- Remove environment bootstrap keys (user_id IS NULL) before restoring the NOT NULL constraint.
DELETE FROM api_keys WHERE user_id IS NULL;
ALTER TABLE api_keys ALTER COLUMN user_id SET NOT NULL;
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
-- Environment bootstrap keys are owned by the system, not a user.
-- Allow user_id to be NULL so agent-side key creation doesn't violate the FK constraint.
ALTER TABLE api_keys ALTER COLUMN user_id DROP NOT NULL;
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
-- Remove environment bootstrap keys (user_id IS NULL) before restoring the NOT NULL constraint.
PRAGMA foreign_keys=OFF;

DROP TABLE IF EXISTS api_keys_old;

CREATE TABLE api_keys_old (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
description TEXT,
key_hash TEXT NOT NULL,
key_prefix TEXT NOT NULL,
managed_by TEXT,
user_id TEXT NOT NULL,
environment_id TEXT,
expires_at DATETIME,
last_used_at DATETIME,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
FOREIGN KEY (environment_id) REFERENCES environments(id) ON DELETE CASCADE
);

INSERT INTO api_keys_old (
id,
name,
description,
key_hash,
key_prefix,
managed_by,
user_id,
environment_id,
expires_at,
last_used_at,
created_at,
updated_at
)
SELECT
id,
name,
description,
key_hash,
key_prefix,
managed_by,
user_id,
environment_id,
expires_at,
last_used_at,
created_at,
updated_at
FROM api_keys
WHERE user_id IS NOT NULL;

DROP TABLE api_keys;
ALTER TABLE api_keys_old RENAME TO api_keys;

CREATE INDEX IF NOT EXISTS idx_api_keys_user_id ON api_keys(user_id);
CREATE INDEX IF NOT EXISTS idx_api_keys_key_hash ON api_keys(key_hash);
CREATE INDEX IF NOT EXISTS idx_api_keys_key_prefix ON api_keys(key_prefix);

PRAGMA foreign_keys=ON;
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
-- Environment bootstrap keys are owned by the system, not a user.
-- Allow user_id to be NULL so agent-side key creation doesn't violate the FK constraint.
PRAGMA foreign_keys=OFF;

DROP TABLE IF EXISTS api_keys_new;

CREATE TABLE api_keys_new (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
description TEXT,
key_hash TEXT NOT NULL,
key_prefix TEXT NOT NULL,
managed_by TEXT,
user_id TEXT,
environment_id TEXT,
expires_at DATETIME,
last_used_at DATETIME,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
FOREIGN KEY (environment_id) REFERENCES environments(id) ON DELETE CASCADE
);

INSERT INTO api_keys_new (
id,
name,
description,
key_hash,
key_prefix,
managed_by,
user_id,
environment_id,
expires_at,
last_used_at,
created_at,
updated_at
)
SELECT
id,
name,
description,
key_hash,
key_prefix,
managed_by,
user_id,
environment_id,
expires_at,
last_used_at,
created_at,
updated_at
FROM api_keys;

DROP TABLE api_keys;
ALTER TABLE api_keys_new RENAME TO api_keys;

CREATE INDEX IF NOT EXISTS idx_api_keys_user_id ON api_keys(user_id);
CREATE INDEX IF NOT EXISTS idx_api_keys_key_hash ON api_keys(key_hash);
CREATE INDEX IF NOT EXISTS idx_api_keys_key_prefix ON api_keys(key_prefix);

PRAGMA foreign_keys=ON;
2 changes: 1 addition & 1 deletion types/apikey/apikey.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ type ApiKey struct {
Name string `json:"name" doc:"Name of the API key"`
Description *string `json:"description,omitempty" doc:"Description of the API key"`
KeyPrefix string `json:"keyPrefix" doc:"Prefix of the API key for identification"`
UserID string `json:"userId" doc:"ID of the user who owns the API key"`
UserID *string `json:"userId,omitempty" doc:"ID of the user who owns the API key"`
IsStatic bool `json:"isStatic" doc:"Whether the API key is environment-managed and protected from deletion"`
ExpiresAt *time.Time `json:"expiresAt,omitempty" doc:"Expiration date of the API key"`
LastUsedAt *time.Time `json:"lastUsedAt,omitempty" doc:"Last time the API key was used"`
Expand Down
Loading