Skip to content

Commit fd77b87

Browse files
authored
feat: add regenerated_at timestamp to PAT (#1542)
1 parent 08d63db commit fd77b87

File tree

14 files changed

+1679
-1626
lines changed

14 files changed

+1679
-1626
lines changed

Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ TAG := $(shell git rev-list --tags --max-count=1)
44
VERSION := $(shell git describe --tags ${TAG})
55
.PHONY: build check fmt lint test test-race vet test-cover-html help install proto admin-app compose-up-dev
66
.DEFAULT_GOAL := build
7-
PROTON_COMMIT := "c3eb9d66b9f6dd04f7d87a7829565be4772f9c47"
7+
PROTON_COMMIT := "3959f2283a9335b800a60eb6fd1cf498c2e2e8aa"
88

99
admin-app:
1010
@echo " > generating admin build"

core/aggregates/orgpats/service.go

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -39,14 +39,15 @@ type CreatedBy struct {
3939
}
4040

4141
type AggregatedPAT struct {
42-
ID string
43-
Title string
44-
CreatedBy CreatedBy
45-
Scopes []patmodels.PATScope
46-
CreatedAt time.Time
47-
ExpiresAt time.Time
48-
LastUsedAt *time.Time
49-
UserID string
42+
ID string
43+
Title string
44+
CreatedBy CreatedBy
45+
Scopes []patmodels.PATScope
46+
CreatedAt time.Time
47+
ExpiresAt time.Time
48+
LastUsedAt *time.Time
49+
RegeneratedAt *time.Time
50+
UserID string
5051
}
5152

5253
// PATSearchFields is used for RQL validation — flat struct with rql tags.
@@ -58,6 +59,7 @@ type PATSearchFields struct {
5859
CreatedAt time.Time `rql:"name=created_at,type=datetime"`
5960
ExpiresAt time.Time `rql:"name=expires_at,type=datetime"`
6061
LastUsedAt *time.Time `rql:"name=last_used_at,type=datetime"`
62+
RegeneratedAt *time.Time `rql:"name=regenerated_at,type=datetime"`
6163
}
6264

6365
type OrganizationPATs struct {

core/userpat/models/pat.go

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -15,17 +15,18 @@ type PATScope struct {
1515
}
1616

1717
type PAT struct {
18-
ID string `rql:"name=id,type=string"`
19-
UserID string `rql:"name=user_id,type=string"`
20-
OrgID string `rql:"name=org_id,type=string"`
21-
Title string `rql:"name=title,type=string"`
22-
SecretHash string `json:"-"`
23-
Metadata metadata.Metadata
24-
Scopes []PATScope
25-
LastUsedAt *time.Time `rql:"name=last_used_at,type=datetime"` // last_used_at can be null
26-
ExpiresAt time.Time `rql:"name=expires_at,type=datetime"`
27-
CreatedAt time.Time `rql:"name=created_at,type=datetime"`
28-
UpdatedAt time.Time `rql:"name=updated_at,type=datetime"`
18+
ID string `rql:"name=id,type=string"`
19+
UserID string `rql:"name=user_id,type=string"`
20+
OrgID string `rql:"name=org_id,type=string"`
21+
Title string `rql:"name=title,type=string"`
22+
SecretHash string `json:"-"`
23+
Metadata metadata.Metadata
24+
Scopes []PATScope
25+
LastUsedAt *time.Time `rql:"name=last_used_at,type=datetime"` // last_used_at can be null
26+
RegeneratedAt *time.Time `rql:"name=regenerated_at,type=datetime"` // regenerated_at can be null
27+
ExpiresAt time.Time `rql:"name=expires_at,type=datetime"`
28+
CreatedAt time.Time `rql:"name=created_at,type=datetime"`
29+
UpdatedAt time.Time `rql:"name=updated_at,type=datetime"`
2930
}
3031

3132
type PATList struct {

core/userpat/service.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -203,7 +203,7 @@ func (s *Service) Regenerate(ctx context.Context, userID, id string, newExpiresA
203203
return patmodels.PAT{}, "", fmt.Errorf("enriching PAT scope: %w", err)
204204
}
205205

206-
if err := s.createAuditRecord(ctx, pkgAuditRecord.PATRegeneratedEvent, regenerated, time.Now().UTC(), map[string]any{
206+
if err := s.createAuditRecord(ctx, pkgAuditRecord.PATRegeneratedEvent, regenerated, *regenerated.RegeneratedAt, map[string]any{
207207
"expires_at": regenerated.ExpiresAt,
208208
"old_expires_at": oldExpiresAt,
209209
}); err != nil {

core/userpat/service_test.go

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2253,14 +2253,16 @@ func TestService_Regenerate(t *testing.T) {
22532253
UpdatedAt: time.Now(),
22542254
}
22552255

2256+
regenTime := time.Now()
22562257
regeneratedPAT := models.PAT{
2257-
ID: "pat-1",
2258-
UserID: "user-1",
2259-
OrgID: "org-1",
2260-
Title: "my-token",
2261-
ExpiresAt: futureExpiry,
2262-
CreatedAt: activePAT.CreatedAt,
2263-
UpdatedAt: time.Now(),
2258+
ID: "pat-1",
2259+
UserID: "user-1",
2260+
OrgID: "org-1",
2261+
Title: "my-token",
2262+
ExpiresAt: futureExpiry,
2263+
RegeneratedAt: &regenTime,
2264+
CreatedAt: activePAT.CreatedAt,
2265+
UpdatedAt: time.Now(),
22642266
}
22652267

22662268
tests := []struct {

internal/api/v1beta1connect/organization_pats.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,9 @@ func transformAggregatedPATToPB(pat svc.AggregatedPAT) *frontierv1beta1.SearchOr
8181
if pat.LastUsedAt != nil {
8282
pbPAT.LastUsedAt = timestamppb.New(*pat.LastUsedAt)
8383
}
84+
if pat.RegeneratedAt != nil {
85+
pbPAT.RegeneratedAt = timestamppb.New(*pat.RegeneratedAt)
86+
}
8487
return pbPAT
8588
}
8689

internal/api/v1beta1connect/user_pat.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -304,6 +304,9 @@ func transformPATToPB(pat models.PAT, patValue string) *frontierv1beta1.PAT {
304304
if pat.LastUsedAt != nil {
305305
pbPAT.LastUsedAt = timestamppb.New(*pat.LastUsedAt)
306306
}
307+
if pat.RegeneratedAt != nil {
308+
pbPAT.RegeneratedAt = timestamppb.New(*pat.RegeneratedAt)
309+
}
307310
if pat.Metadata != nil {
308311
metaPB, err := pat.Metadata.ToStructPB()
309312
if err == nil {
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
ALTER TABLE user_pats DROP COLUMN IF EXISTS regenerated_at;
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
ALTER TABLE user_pats ADD COLUMN regenerated_at TIMESTAMPTZ;

internal/store/postgres/org_pats_repository.go

Lines changed: 20 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ var orgPATFilterFields = map[string]string{
3636
"created_at": "p.created_at",
3737
"expires_at": "p.expires_at",
3838
"last_used_at": "p.last_used_at",
39+
"regenerated_at": "p.regenerated_at",
3940
}
4041

4142
// orgPATSearchColumns are searched with ILIKE when RQL search is used.
@@ -54,6 +55,7 @@ var orgPATSortFields = map[string]string{
5455
"created_at": "p.created_at",
5556
"expires_at": "p.expires_at",
5657
"last_used_at": "p.last_used_at",
58+
"regenerated_at": "p.regenerated_at",
5759
}
5860

5961
// OrgPATRow is the flat SQL result row from the joined query.
@@ -67,6 +69,7 @@ type OrgPATRow struct {
6769
CreatedAt time.Time `db:"pat_created_at"`
6870
ExpiresAt time.Time `db:"pat_expires_at"`
6971
LastUsedAt *time.Time `db:"pat_last_used_at"`
72+
RegeneratedAt *time.Time `db:"pat_regenerated_at"`
7073
RoleID *string `db:"role_id"`
7174
ResourceType *string `db:"resource_type"`
7275
ResourceID *string `db:"resource_id"`
@@ -177,7 +180,7 @@ func (r OrgPATsRepository) buildDataQuery(orgID string, rqlQuery *rql.Query) (st
177180
paginatedInner := inner.
178181
Select(
179182
goqu.I("p.id"), goqu.I("p.title"), goqu.I("p.user_id"),
180-
goqu.I("p.created_at"), goqu.I("p.expires_at"), goqu.I("p.last_used_at"),
183+
goqu.I("p.created_at"), goqu.I("p.expires_at"), goqu.I("p.last_used_at"), goqu.I("p.regenerated_at"),
181184
).
182185
Offset(uint(rqlQuery.Offset)).
183186
Limit(uint(rqlQuery.Limit))
@@ -192,6 +195,7 @@ func (r OrgPATsRepository) buildDataQuery(orgID string, rqlQuery *rql.Query) (st
192195
goqu.I("p.created_at").As("pat_created_at"),
193196
goqu.I("p.expires_at").As("pat_expires_at"),
194197
goqu.I("p.last_used_at").As("pat_last_used_at"),
198+
goqu.I("p.regenerated_at").As("pat_regenerated_at"),
195199
goqu.I("pol.role_id"),
196200
goqu.I("pol.resource_type"),
197201
goqu.I("pol.resource_id"),
@@ -207,13 +211,18 @@ func (r OrgPATsRepository) buildDataQuery(orgID string, rqlQuery *rql.Query) (st
207211
goqu.I("pol.principal_id").Eq(goqu.I("p.id")),
208212
goqu.I("pol.principal_type").Eq(schema.PATPrincipal),
209213
),
210-
).
211-
Order(
212-
goqu.I("p.created_at").Desc(),
213-
goqu.I("p.id").Asc(),
214-
goqu.I("pol.role_id").Asc(),
215214
)
216215

216+
// Apply the same sort to the outer query to preserve the user's requested order.
217+
// The inner query sort controls pagination (which rows), the outer sort controls
218+
// final row order after the policy LEFT JOIN expands rows.
219+
outer, err = r.addSort(outer, rqlQuery.Sort)
220+
if err != nil {
221+
return "", nil, err
222+
}
223+
// Add deterministic tiebreakers
224+
outer = outer.OrderAppend(goqu.I("p.id").Asc(), goqu.I("pol.role_id").Asc())
225+
217226
return outer.ToSQL()
218227
}
219228

@@ -297,10 +306,11 @@ func (r OrgPATsRepository) groupRows(rows []OrgPATRow) []svc.AggregatedPAT {
297306
Title: row.CreatedByTitle,
298307
Email: row.CreatedByEmail,
299308
},
300-
CreatedAt: row.CreatedAt,
301-
ExpiresAt: row.ExpiresAt,
302-
LastUsedAt: row.LastUsedAt,
303-
UserID: row.CreatedByID,
309+
CreatedAt: row.CreatedAt,
310+
ExpiresAt: row.ExpiresAt,
311+
LastUsedAt: row.LastUsedAt,
312+
RegeneratedAt: row.RegeneratedAt,
313+
UserID: row.CreatedByID,
304314
}
305315
patMap[row.PATID] = pat
306316
patOrder = append(patOrder, row.PATID)

0 commit comments

Comments
 (0)