Skip to content

Commit e663da8

Browse files
authored
fix: Fix api_keys path, cleanup unused fields (#29)
* fix: Fix api_keys path, cleanup unused fields * fix: Review comment
1 parent 80c998f commit e663da8

11 files changed

Lines changed: 115 additions & 32 deletions

File tree

domain/apikey.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ type APIKey struct {
3636
IdentityID string `bun:"identity_id,type:uuid" json:"identity_id"`
3737
CreatedBy string `bun:"created_by" json:"created_by"`
3838
Scopes []string `bun:"scopes,array" json:"scopes"`
39+
Product string `bun:"product" json:"product"`
3940
Environment string `bun:"environment" json:"environment"`
4041
ExpiresAt *time.Time `bun:"expires_at" json:"expires_at,omitempty"`
4142
State string `bun:"state" json:"state"`

domain/token.go

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -79,9 +79,6 @@ type OAuthClient struct {
7979
GrantTypes []string `bun:"grant_types,array" json:"grant_types"`
8080
RedirectURIs []string `bun:"redirect_uris,array" json:"redirect_uris"`
8181
Scopes []string `bun:"scopes,array" json:"scopes"`
82-
// IsMCP marks MCP clients — they receive short-lived (1h) access tokens
83-
// plus a rotating refresh token instead of the 90-day CLI token.
84-
IsMCP bool `bun:"is_mcp" json:"is_mcp"`
8582
IsActive bool `bun:"is_active" json:"is_active"`
8683
CreatedAt time.Time `bun:"created_at" json:"created_at"`
8784
UpdatedAt time.Time `bun:"updated_at" json:"updated_at"`

internal/handler/apikey.go

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ type CreateAPIKeyInput struct {
2020
Name string `json:"name" required:"true" minLength:"1" doc:"Human-readable key name"`
2121
Description string `json:"description,omitempty" doc:"Key description"`
2222
IdentityID string `json:"identity_id,omitempty" doc:"Optional identity link"`
23+
Product string `json:"product,omitempty" doc:"Product namespace for key scoping"`
2324
Scopes []string `json:"scopes,omitempty" doc:"Allowed scopes"`
2425
Environment string `json:"environment,omitempty" enum:"live,test" doc:"Environment (default: live)"`
2526
ExpiresInDays *int `json:"expires_in_days,omitempty" doc:"Expiry in days (nil = never)"`
@@ -40,8 +41,11 @@ type APIKeyOutput struct {
4041
}
4142

4243
type APIKeyListInput struct {
43-
Page int `query:"page" default:"1" doc:"Page number"`
44-
Limit int `query:"limit" default:"20" doc:"Items per page (max 100)"`
44+
Product string `query:"product" doc:"Filter by product namespace"`
45+
ApplicationID string `query:"application_id" doc:"Filter by application identity ID"`
46+
Label string `query:"label" doc:"Filter by identity label (key:value, e.g. env:production)"`
47+
Page int `query:"page" default:"1" doc:"Page number"`
48+
Limit int `query:"limit" default:"20" doc:"Items per page (max 100)"`
4549
}
4650

4751
type APIKeyListOutput struct {
@@ -118,6 +122,7 @@ func (a *API) createAPIKeyOp(ctx context.Context, input *CreateAPIKeyInput) (*Cr
118122
Name: input.Body.Name,
119123
Description: input.Body.Description,
120124
IdentityID: input.Body.IdentityID,
125+
Product: input.Body.Product,
121126
Scopes: input.Body.Scopes,
122127
Environment: input.Body.Environment,
123128
ExpiresInDays: input.Body.ExpiresInDays,
@@ -150,7 +155,7 @@ func (a *API) listAPIKeysOp(ctx context.Context, input *APIKeyListInput) (*APIKe
150155
return nil, huma.Error401Unauthorized("missing tenant context")
151156
}
152157

153-
keys, total, err := a.apiKeySvc.ListKeys(ctx, tenant.AccountID, tenant.ProjectID, "", "", input.Page, input.Limit)
158+
keys, total, err := a.apiKeySvc.ListKeys(ctx, tenant.AccountID, tenant.ProjectID, input.ApplicationID, input.Product, input.Label, input.Page, input.Limit)
154159
if err != nil {
155160
log.Error().Err(err).Msg("failed to list API keys")
156161
return nil, huma.Error500InternalServerError("failed to list API keys")
@@ -159,6 +164,7 @@ func (a *API) listAPIKeysOp(ctx context.Context, input *APIKeyListInput) (*APIKe
159164
if keys == nil {
160165
keys = []*domain.APIKey{}
161166
}
167+
162168
out := &APIKeyListOutput{}
163169
out.Body.Keys = keys
164170
out.Body.Total = total

internal/service/apikey.go

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ type CreateAPIKeyRequest struct {
4242
Description string
4343
IdentityID string
4444
CredentialPolicyID string // Optional — if empty, the tenant's default policy is assigned.
45+
Product string
4546
Scopes []string
4647
Environment string
4748
ExpiresInDays *int
@@ -63,9 +64,20 @@ type CreateAPIKeyResponse struct {
6364

6465
// CreateKey generates a new API key, hashes it, stores the hash, and returns the full key once.
6566
// Every key is linked to an identity and assigned a credential policy.
66-
// If IdentityID is empty, no identity link is set.
67+
// If IdentityID is empty and Product is set, a service identity is auto-provisioned
68+
// (or reused if one already exists for this account+project+product).
6769
// If CredentialPolicyID is empty, the tenant's default policy is auto-created and assigned.
6870
func (s *APIKeyService) CreateKey(ctx context.Context, req CreateAPIKeyRequest) (*CreateAPIKeyResponse, error) {
71+
// Ensure every key has an identity link.
72+
// When no identity is provided, auto-provision a service identity for the product.
73+
if req.IdentityID == "" && req.Product != "" {
74+
identity, err := s.identitySvc.EnsureServiceIdentity(ctx, req.AccountID, req.ProjectID, req.Product, req.CreatedBy)
75+
if err != nil {
76+
return nil, fmt.Errorf("failed to ensure service identity for product %s: %w", req.Product, err)
77+
}
78+
req.IdentityID = identity.ID
79+
}
80+
6981
// Ensure the key has a credential policy.
7082
policyID := req.CredentialPolicyID
7183
if policyID == "" {
@@ -114,6 +126,7 @@ func (s *APIKeyService) CreateKey(ctx context.Context, req CreateAPIKeyRequest)
114126
IdentityID: req.IdentityID,
115127
CreatedBy: req.CreatedBy,
116128
CredentialPolicyID: policyID,
129+
Product: req.Product,
117130
Scopes: scopes,
118131
Environment: env,
119132
ExpiresAt: expiresAt,
@@ -145,7 +158,7 @@ func (s *APIKeyService) CreateKey(ctx context.Context, req CreateAPIKeyRequest)
145158
}
146159

147160
// ListKeys returns paginated API keys for an account/project.
148-
func (s *APIKeyService) ListKeys(ctx context.Context, accountID, projectID, applicationID, product string, page, limit int) ([]*domain.APIKey, int, error) {
161+
func (s *APIKeyService) ListKeys(ctx context.Context, accountID, projectID, applicationID, product, label string, page, limit int) ([]*domain.APIKey, int, error) {
149162
if page < 1 {
150163
page = 1
151164
}
@@ -154,7 +167,7 @@ func (s *APIKeyService) ListKeys(ctx context.Context, accountID, projectID, appl
154167
}
155168
offset := (page - 1) * limit
156169

157-
return s.repo.ListByAccountProject(ctx, accountID, projectID, applicationID, product, limit, offset)
170+
return s.repo.ListByAccountProject(ctx, accountID, projectID, applicationID, product, label, limit, offset)
158171
}
159172

160173
// GetKey returns an API key by ID.

internal/store/postgres/apikey.go

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@ package postgres
22

33
import (
44
"context"
5+
"encoding/json"
56
"fmt"
7+
"strings"
68
"time"
79

810
"github.com/uptrace/bun"
@@ -68,25 +70,36 @@ func (r *APIKeyRepository) GetByID(ctx context.Context, id string) (*domain.APIK
6870
}
6971

7072
// ListByAccountProject returns paginated API keys for an account/project,
71-
// optionally filtered by application ID.
72-
func (r *APIKeyRepository) ListByAccountProject(ctx context.Context, accountID, projectID, applicationID, product string, limit, offset int) ([]*domain.APIKey, int, error) {
73+
// optionally filtered by application ID, product, or identity label.
74+
// The label parameter accepts "key:value" format (e.g. "env:production")
75+
// and filters by joining on the identities table using JSONB containment.
76+
func (r *APIKeyRepository) ListByAccountProject(ctx context.Context, accountID, projectID, applicationID, product, label string, limit, offset int) ([]*domain.APIKey, int, error) {
7377
var keys []*domain.APIKey
7478

7579
q := r.db.NewSelect().
7680
Model(&keys).
77-
Where("account_id = ?", accountID).
78-
OrderExpr("created_at DESC").
81+
Where("sk.account_id = ?", accountID).
82+
OrderExpr("sk.created_at DESC").
7983
Limit(limit).
8084
Offset(offset)
8185

8286
if projectID != "" {
83-
q = q.Where("project_id = ?", projectID)
87+
q = q.Where("sk.project_id = ?", projectID)
8488
}
8589
if applicationID != "" {
86-
q = q.Where("identity_id = ?", applicationID)
90+
q = q.Where("sk.identity_id = ?", applicationID)
8791
}
8892
if product != "" {
89-
q = q.Where("product = ?", product)
93+
q = q.Where("sk.product = ?", product)
94+
}
95+
if label != "" {
96+
parts := strings.SplitN(label, ":", 2)
97+
if len(parts) == 2 && parts[0] != "" {
98+
labelJSON, _ := json.Marshal(map[string]string{parts[0]: parts[1]})
99+
100+
q = q.Join("JOIN identities AS i ON i.id = sk.identity_id").
101+
Where("i.labels @> ?::jsonb", string(labelJSON))
102+
}
90103
}
91104

92105
count, err := q.ScanAndCount(ctx)

internal/store/postgres/identity.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package postgres
22

33
import (
44
"context"
5+
"encoding/json"
56
"fmt"
67
"strings"
78

@@ -91,7 +92,8 @@ func (r *IdentityRepository) List(ctx context.Context, accountID, projectID stri
9192
if len(parts) != 2 || parts[0] == "" {
9293
return nil, fmt.Errorf("invalid label format: expected non-empty-key:value, got %q", label)
9394
}
94-
q = q.Where("labels @> ?::jsonb", fmt.Sprintf(`{"%s": "%s"}`, parts[0], parts[1]))
95+
labelJSON, _ := json.Marshal(map[string]string{parts[0]: parts[1]})
96+
q = q.Where("labels @> ?::jsonb", string(labelJSON))
9597
}
9698

9799
if err := q.Scan(ctx); err != nil {

migrations/001_init_schema.up.sql

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -145,7 +145,6 @@ CREATE TABLE IF NOT EXISTS refresh_tokens (
145145
token_hash TEXT NOT NULL UNIQUE,
146146
client_id TEXT NOT NULL,
147147
account_id VARCHAR(255) NOT NULL,
148-
gateway_id TEXT DEFAULT '',
149148
project_id VARCHAR(255) DEFAULT '',
150149
user_id TEXT NOT NULL,
151150
identity_id UUID REFERENCES identities(id) ON DELETE SET NULL,

migrations/006_service_keys.up.sql

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,6 @@ CREATE TABLE IF NOT EXISTS service_keys (
2525
last_used_at TIMESTAMPTZ,
2626
last_used_ip TEXT DEFAULT '',
2727
usage_count BIGINT NOT NULL DEFAULT 0,
28-
gateway_id TEXT DEFAULT '',
29-
user_email TEXT DEFAULT '',
30-
user_name TEXT DEFAULT '',
3128
metadata JSONB DEFAULT '{}',
3229
ip_allowlist TEXT[] DEFAULT '{}',
3330
credential_policy_id UUID REFERENCES credential_policies(id),

migrations/007_oauth_clients_is_mcp.down.sql

Lines changed: 0 additions & 2 deletions
This file was deleted.

migrations/007_oauth_clients_is_mcp.up.sql

Lines changed: 0 additions & 9 deletions
This file was deleted.

0 commit comments

Comments
 (0)