Skip to content

Commit 5664cda

Browse files
committed
feat(storage): implement ServiceAccount+TrustedIssuer CRUD for NoSQL providers
Replace the placeholder stubs in mongodb, arangodb, cassandradb, dynamodb, and couchbase with real implementations matching the SQL reference semantics. - UUID id generation and Key=ID parity on create; unix CreatedAt/UpdatedAt - Update rejects a partial struct (CreatedAt==0 guard) per provider - DeleteServiceAccount cascades to its TrustedIssuer rows - ListTrustedIssuers optionally filters by serviceAccountID; pagination returns {Limit, Page, Offset, Total} - GetTrustedIssuerByIssuerURL is an indexed/keyed lookup (client_assertion hot path): mongo/arango/cassandra/couchbase secondary index, dynamo GSI - Per-provider collection/table setup added where the provider requires it (mongo CreateCollection+indexes, arango CollectionExists+EnsureHashIndex, cassandra CREATE TABLE + secondary indexes, dynamo ensureTables entries); couchbase collections+primary indexes auto-create via reflection, only secondary indexes added
1 parent b6f3e3c commit 5664cda

15 files changed

Lines changed: 1248 additions & 191 deletions

internal/storage/db/arangodb/provider.go

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -348,6 +348,41 @@ func NewProvider(cfg *config.Config, deps *Dependencies) (*provider, error) {
348348
Sparse: true,
349349
})
350350

351+
// ServiceAccount collection
352+
serviceAccountCollectionExists, err := arangodb.CollectionExists(ctx, schemas.Collections.ServiceAccount)
353+
if err != nil {
354+
return nil, err
355+
}
356+
if !serviceAccountCollectionExists {
357+
_, err = arangodb.CreateCollection(ctx, schemas.Collections.ServiceAccount, nil)
358+
if err != nil {
359+
return nil, err
360+
}
361+
}
362+
363+
// TrustedIssuer collection and indexes
364+
trustedIssuerCollectionExists, err := arangodb.CollectionExists(ctx, schemas.Collections.TrustedIssuer)
365+
if err != nil {
366+
return nil, err
367+
}
368+
if !trustedIssuerCollectionExists {
369+
_, err = arangodb.CreateCollection(ctx, schemas.Collections.TrustedIssuer, nil)
370+
if err != nil {
371+
return nil, err
372+
}
373+
}
374+
trustedIssuerCollection, err := arangodb.Collection(ctx, schemas.Collections.TrustedIssuer)
375+
if err != nil {
376+
return nil, err
377+
}
378+
_, _, _ = trustedIssuerCollection.EnsureHashIndex(ctx, []string{"issuer_url"}, &arangoDriver.EnsureHashIndexOptions{
379+
Unique: true,
380+
Sparse: true,
381+
})
382+
_, _, _ = trustedIssuerCollection.EnsureHashIndex(ctx, []string{"service_account_id"}, &arangoDriver.EnsureHashIndexOptions{
383+
Sparse: true,
384+
})
385+
351386
return &provider{
352387
config: cfg,
353388
dependencies: deps,

internal/storage/db/arangodb/service_account.go

Lines changed: 101 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -3,37 +3,122 @@ package arangodb
33
import (
44
"context"
55
"fmt"
6+
"time"
7+
8+
arangoDriver "github.com/arangodb/go-driver"
9+
"github.com/google/uuid"
610

711
"github.com/authorizerdev/authorizer/internal/graph/model"
812
"github.com/authorizerdev/authorizer/internal/storage/schemas"
913
)
1014

1115
// AddServiceAccount creates a new service account record.
12-
// TODO(phase1-pr3): implement ArangoDB provider.
13-
func (p *provider) AddServiceAccount(_ context.Context, _ *schemas.ServiceAccount) (*schemas.ServiceAccount, error) {
14-
return nil, fmt.Errorf("arangodb: AddServiceAccount not implemented")
16+
func (p *provider) AddServiceAccount(ctx context.Context, sa *schemas.ServiceAccount) (*schemas.ServiceAccount, error) {
17+
if sa.ID == "" {
18+
sa.ID = uuid.New().String()
19+
}
20+
sa.Key = sa.ID
21+
now := time.Now().Unix()
22+
sa.CreatedAt = now
23+
sa.UpdatedAt = now
24+
saCollection, _ := p.db.Collection(ctx, schemas.Collections.ServiceAccount)
25+
meta, err := saCollection.CreateDocument(ctx, sa)
26+
if err != nil {
27+
return nil, err
28+
}
29+
sa.Key = meta.Key
30+
sa.ID = meta.ID.String()
31+
return sa, nil
1532
}
1633

1734
// UpdateServiceAccount updates a service account record.
18-
// TODO(phase1-pr3): implement ArangoDB provider.
19-
func (p *provider) UpdateServiceAccount(_ context.Context, _ *schemas.ServiceAccount) (*schemas.ServiceAccount, error) {
20-
return nil, fmt.Errorf("arangodb: UpdateServiceAccount not implemented")
35+
// Callers MUST load the existing record and mutate it before calling this
36+
// method — the document replace writes every field.
37+
func (p *provider) UpdateServiceAccount(ctx context.Context, sa *schemas.ServiceAccount) (*schemas.ServiceAccount, error) {
38+
if sa.CreatedAt == 0 {
39+
return nil, fmt.Errorf("UpdateServiceAccount: caller must load record before updating (CreatedAt is zero — partial struct detected)")
40+
}
41+
sa.UpdatedAt = time.Now().Unix()
42+
saCollection, _ := p.db.Collection(ctx, schemas.Collections.ServiceAccount)
43+
meta, err := saCollection.UpdateDocument(ctx, sa.Key, sa)
44+
if err != nil {
45+
return nil, err
46+
}
47+
sa.Key = meta.Key
48+
sa.ID = meta.ID.String()
49+
return sa, nil
2150
}
2251

23-
// DeleteServiceAccount removes a service account record.
24-
// TODO(phase1-pr3): implement ArangoDB provider.
25-
func (p *provider) DeleteServiceAccount(_ context.Context, _ *schemas.ServiceAccount) error {
26-
return fmt.Errorf("arangodb: DeleteServiceAccount not implemented")
52+
// DeleteServiceAccount removes a service account and all its associated
53+
// TrustedIssuers. Mirrors the webhook cascade-delete pattern.
54+
func (p *provider) DeleteServiceAccount(ctx context.Context, sa *schemas.ServiceAccount) error {
55+
saCollection, _ := p.db.Collection(ctx, schemas.Collections.ServiceAccount)
56+
_, err := saCollection.RemoveDocument(ctx, sa.Key)
57+
if err != nil {
58+
return err
59+
}
60+
query := fmt.Sprintf("FOR d IN %s FILTER d.service_account_id == @service_account_id REMOVE { _key: d._key } IN %s", schemas.Collections.TrustedIssuer, schemas.Collections.TrustedIssuer)
61+
bindVars := map[string]interface{}{
62+
"service_account_id": sa.ID,
63+
}
64+
cursor, err := p.db.Query(ctx, query, bindVars)
65+
if err != nil {
66+
return err
67+
}
68+
defer func() { _ = cursor.Close() }()
69+
return nil
2770
}
2871

2972
// GetServiceAccountByID fetches a service account by primary key.
30-
// TODO(phase1-pr3): implement ArangoDB provider.
31-
func (p *provider) GetServiceAccountByID(_ context.Context, _ string) (*schemas.ServiceAccount, error) {
32-
return nil, fmt.Errorf("arangodb: GetServiceAccountByID not implemented")
73+
func (p *provider) GetServiceAccountByID(ctx context.Context, id string) (*schemas.ServiceAccount, error) {
74+
var sa *schemas.ServiceAccount
75+
query := fmt.Sprintf("FOR d in %s FILTER d._id == @id LIMIT 1 RETURN d", schemas.Collections.ServiceAccount)
76+
bindVars := map[string]interface{}{
77+
"id": id,
78+
}
79+
cursor, err := p.db.Query(ctx, query, bindVars)
80+
if err != nil {
81+
return nil, err
82+
}
83+
defer func() { _ = cursor.Close() }()
84+
for {
85+
if !cursor.HasMore() {
86+
if sa == nil {
87+
return nil, fmt.Errorf("service account not found")
88+
}
89+
break
90+
}
91+
_, err := cursor.ReadDocument(ctx, &sa)
92+
if err != nil {
93+
return nil, err
94+
}
95+
}
96+
return sa, nil
3397
}
3498

3599
// ListServiceAccounts returns a paginated list of service accounts.
36-
// TODO(phase1-pr3): implement ArangoDB provider.
37-
func (p *provider) ListServiceAccounts(_ context.Context, _ *model.Pagination) ([]*schemas.ServiceAccount, *model.Pagination, error) {
38-
return nil, nil, fmt.Errorf("arangodb: ListServiceAccounts not implemented")
100+
func (p *provider) ListServiceAccounts(ctx context.Context, pagination *model.Pagination) ([]*schemas.ServiceAccount, *model.Pagination, error) {
101+
serviceAccounts := []*schemas.ServiceAccount{}
102+
query := fmt.Sprintf("FOR d in %s SORT d.created_at DESC LIMIT %d, %d RETURN d", schemas.Collections.ServiceAccount, pagination.Offset, pagination.Limit)
103+
sctx := arangoDriver.WithQueryFullCount(ctx)
104+
cursor, err := p.db.Query(sctx, query, nil)
105+
if err != nil {
106+
return nil, nil, err
107+
}
108+
defer func() { _ = cursor.Close() }()
109+
paginationClone := pagination
110+
paginationClone.Total = cursor.Statistics().FullCount()
111+
for {
112+
var sa *schemas.ServiceAccount
113+
meta, err := cursor.ReadDocument(ctx, &sa)
114+
if arangoDriver.IsNoMoreDocuments(err) {
115+
break
116+
} else if err != nil {
117+
return nil, nil, err
118+
}
119+
if meta.Key != "" {
120+
serviceAccounts = append(serviceAccounts, sa)
121+
}
122+
}
123+
return serviceAccounts, paginationClone, nil
39124
}

internal/storage/db/arangodb/trusted_issuer.go

Lines changed: 121 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -3,43 +3,146 @@ package arangodb
33
import (
44
"context"
55
"fmt"
6+
"time"
7+
8+
arangoDriver "github.com/arangodb/go-driver"
9+
"github.com/google/uuid"
610

711
"github.com/authorizerdev/authorizer/internal/graph/model"
812
"github.com/authorizerdev/authorizer/internal/storage/schemas"
913
)
1014

1115
// AddTrustedIssuer creates a new trusted issuer record.
12-
// TODO(phase1-pr3): implement ArangoDB provider.
13-
func (p *provider) AddTrustedIssuer(_ context.Context, _ *schemas.TrustedIssuer) (*schemas.TrustedIssuer, error) {
14-
return nil, fmt.Errorf("arangodb: AddTrustedIssuer not implemented")
16+
func (p *provider) AddTrustedIssuer(ctx context.Context, issuer *schemas.TrustedIssuer) (*schemas.TrustedIssuer, error) {
17+
if issuer.ID == "" {
18+
issuer.ID = uuid.New().String()
19+
}
20+
issuer.Key = issuer.ID
21+
now := time.Now().Unix()
22+
issuer.CreatedAt = now
23+
issuer.UpdatedAt = now
24+
issuerCollection, _ := p.db.Collection(ctx, schemas.Collections.TrustedIssuer)
25+
meta, err := issuerCollection.CreateDocument(ctx, issuer)
26+
if err != nil {
27+
return nil, err
28+
}
29+
issuer.Key = meta.Key
30+
issuer.ID = meta.ID.String()
31+
return issuer, nil
1532
}
1633

1734
// UpdateTrustedIssuer updates a trusted issuer record.
18-
// TODO(phase1-pr3): implement ArangoDB provider.
19-
func (p *provider) UpdateTrustedIssuer(_ context.Context, _ *schemas.TrustedIssuer) (*schemas.TrustedIssuer, error) {
20-
return nil, fmt.Errorf("arangodb: UpdateTrustedIssuer not implemented")
35+
// Callers MUST load the existing record and mutate it before calling this
36+
// method — the document replace writes every field.
37+
func (p *provider) UpdateTrustedIssuer(ctx context.Context, issuer *schemas.TrustedIssuer) (*schemas.TrustedIssuer, error) {
38+
if issuer.CreatedAt == 0 {
39+
return nil, fmt.Errorf("UpdateTrustedIssuer: caller must load record before updating (CreatedAt is zero — partial struct detected)")
40+
}
41+
issuer.UpdatedAt = time.Now().Unix()
42+
issuerCollection, _ := p.db.Collection(ctx, schemas.Collections.TrustedIssuer)
43+
meta, err := issuerCollection.UpdateDocument(ctx, issuer.Key, issuer)
44+
if err != nil {
45+
return nil, err
46+
}
47+
issuer.Key = meta.Key
48+
issuer.ID = meta.ID.String()
49+
return issuer, nil
2150
}
2251

2352
// DeleteTrustedIssuer removes a trusted issuer record.
24-
// TODO(phase1-pr3): implement ArangoDB provider.
25-
func (p *provider) DeleteTrustedIssuer(_ context.Context, _ *schemas.TrustedIssuer) error {
26-
return fmt.Errorf("arangodb: DeleteTrustedIssuer not implemented")
53+
func (p *provider) DeleteTrustedIssuer(ctx context.Context, issuer *schemas.TrustedIssuer) error {
54+
issuerCollection, _ := p.db.Collection(ctx, schemas.Collections.TrustedIssuer)
55+
_, err := issuerCollection.RemoveDocument(ctx, issuer.Key)
56+
if err != nil {
57+
return err
58+
}
59+
return nil
2760
}
2861

2962
// GetTrustedIssuerByID fetches a trusted issuer by primary key.
30-
// TODO(phase1-pr3): implement ArangoDB provider.
31-
func (p *provider) GetTrustedIssuerByID(_ context.Context, _ string) (*schemas.TrustedIssuer, error) {
32-
return nil, fmt.Errorf("arangodb: GetTrustedIssuerByID not implemented")
63+
func (p *provider) GetTrustedIssuerByID(ctx context.Context, id string) (*schemas.TrustedIssuer, error) {
64+
var issuer *schemas.TrustedIssuer
65+
query := fmt.Sprintf("FOR d in %s FILTER d._id == @id LIMIT 1 RETURN d", schemas.Collections.TrustedIssuer)
66+
bindVars := map[string]interface{}{
67+
"id": id,
68+
}
69+
cursor, err := p.db.Query(ctx, query, bindVars)
70+
if err != nil {
71+
return nil, err
72+
}
73+
defer func() { _ = cursor.Close() }()
74+
for {
75+
if !cursor.HasMore() {
76+
if issuer == nil {
77+
return nil, fmt.Errorf("trusted issuer not found")
78+
}
79+
break
80+
}
81+
_, err := cursor.ReadDocument(ctx, &issuer)
82+
if err != nil {
83+
return nil, err
84+
}
85+
}
86+
return issuer, nil
3387
}
3488

3589
// GetTrustedIssuerByIssuerURL fetches a trusted issuer by its unique issuer URL.
36-
// TODO(phase1-pr3): implement ArangoDB provider.
37-
func (p *provider) GetTrustedIssuerByIssuerURL(_ context.Context, _ string) (*schemas.TrustedIssuer, error) {
38-
return nil, fmt.Errorf("arangodb: GetTrustedIssuerByIssuerURL not implemented")
90+
// Called on every client_assertion validation — kept as a single indexed lookup.
91+
func (p *provider) GetTrustedIssuerByIssuerURL(ctx context.Context, issuerURL string) (*schemas.TrustedIssuer, error) {
92+
var issuer *schemas.TrustedIssuer
93+
query := fmt.Sprintf("FOR d in %s FILTER d.issuer_url == @issuer_url LIMIT 1 RETURN d", schemas.Collections.TrustedIssuer)
94+
bindVars := map[string]interface{}{
95+
"issuer_url": issuerURL,
96+
}
97+
cursor, err := p.db.Query(ctx, query, bindVars)
98+
if err != nil {
99+
return nil, err
100+
}
101+
defer func() { _ = cursor.Close() }()
102+
for {
103+
if !cursor.HasMore() {
104+
if issuer == nil {
105+
return nil, fmt.Errorf("trusted issuer not found")
106+
}
107+
break
108+
}
109+
_, err := cursor.ReadDocument(ctx, &issuer)
110+
if err != nil {
111+
return nil, err
112+
}
113+
}
114+
return issuer, nil
39115
}
40116

41117
// ListTrustedIssuers returns paginated trusted issuers, optionally filtered by serviceAccountID.
42-
// TODO(phase1-pr3): implement ArangoDB provider.
43-
func (p *provider) ListTrustedIssuers(_ context.Context, _ string, _ *model.Pagination) ([]*schemas.TrustedIssuer, *model.Pagination, error) {
44-
return nil, nil, fmt.Errorf("arangodb: ListTrustedIssuers not implemented")
118+
func (p *provider) ListTrustedIssuers(ctx context.Context, serviceAccountID string, pagination *model.Pagination) ([]*schemas.TrustedIssuer, *model.Pagination, error) {
119+
issuers := []*schemas.TrustedIssuer{}
120+
filter := ""
121+
bindVars := map[string]interface{}{}
122+
if serviceAccountID != "" {
123+
filter = "FILTER d.service_account_id == @service_account_id "
124+
bindVars["service_account_id"] = serviceAccountID
125+
}
126+
query := fmt.Sprintf("FOR d in %s %sSORT d.created_at DESC LIMIT %d, %d RETURN d", schemas.Collections.TrustedIssuer, filter, pagination.Offset, pagination.Limit)
127+
sctx := arangoDriver.WithQueryFullCount(ctx)
128+
cursor, err := p.db.Query(sctx, query, bindVars)
129+
if err != nil {
130+
return nil, nil, err
131+
}
132+
defer func() { _ = cursor.Close() }()
133+
paginationClone := pagination
134+
paginationClone.Total = cursor.Statistics().FullCount()
135+
for {
136+
var issuer *schemas.TrustedIssuer
137+
meta, err := cursor.ReadDocument(ctx, &issuer)
138+
if arangoDriver.IsNoMoreDocuments(err) {
139+
break
140+
} else if err != nil {
141+
return nil, nil, err
142+
}
143+
if meta.Key != "" {
144+
issuers = append(issuers, issuer)
145+
}
146+
}
147+
return issuers, paginationClone, nil
45148
}

0 commit comments

Comments
 (0)