Skip to content

feat: add Personal Access Tokens (PAT) for CLI/API authentication#495

Open
rossigee wants to merge 9 commits into
container-registry:mainfrom
rossigee:feature/personal-access-tokens-pr
Open

feat: add Personal Access Tokens (PAT) for CLI/API authentication#495
rossigee wants to merge 9 commits into
container-registry:mainfrom
rossigee:feature/personal-access-tokens-pr

Conversation

@rossigee

@rossigee rossigee commented Jul 21, 2026

Copy link
Copy Markdown
Contributor

Summary

Introduces a Personal Access Token (PAT) system as a replacement for the OIDC CLI secret, providing named, expiring, auditable tokens for CLI/API authentication.

  • Backend: New personal_access_token table, full CRUD API under /api/v2.0/users/{id}/personal_access_tokens, PBKDF2-SHA256 hashing with salt, security middleware integration for HTTP Basic Auth
  • Migration: Existing OIDC CLI secrets automatically migrated to legacy PAT records on startup — no user action required
  • Portal: Standalone "Personal Access Tokens" modal (opened from the user dropdown menu, alongside "User Profile" and "Preferences") for create/list/delete/enable/disable/refresh with copy-to-clipboard support. The CLI secret section in the User Profile modal is unchanged; PAT management was not folded into that modal since its own PAT-management UI had grown too large to sit comfortably as a tab there.
  • Auth: Token format hbr_pat_<32-char-random>, full user permissions (v1), configurable TTL or never-expire (-1), last_used_at audit trail

Token format

hbr_pat_<32-char-random>

Storage: PBKDF2-SHA256 hash with salt (consistent with robot accounts)

Related

Test plan

  • Create a PAT via the UI and verify the token is displayed once
  • Use the PAT for docker login — login should succeed
  • Verify existing OIDC CLI secrets are auto-migrated to PATs on startup
  • Delete/disable a PAT and verify it no longer authenticates
  • Refresh a PAT secret and verify the old secret no longer authenticates
  • Verify expired PATs are rejected
  • Unit tests: src/pkg/pat/, src/controller/pat/, src/server/middleware/security/pat_test.go

@gitar-bot

gitar-bot Bot commented Jul 21, 2026

Copy link
Copy Markdown

Gitar is working

Gitar

@coderabbitai

coderabbitai Bot commented Jul 21, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

This PR adds Personal Access Token storage, hashing, scoped authorization, REST APIs, authentication middleware, legacy CLI-secret migration, portal management UI, localization, persisted external-auth group memberships, and integration tests.

Changes

PAT storage and persistence

Layer / File(s) Summary
PAT storage and persistence
make/migrations/postgresql/*, src/pkg/pat/model/*, src/pkg/pat/dao/*, src/pkg/pat/manager.go
Adds the PAT table, scope column, model, DAO, manager, constraints, indexes, CRUD operations, and persistence tests.

PAT creation and scope enforcement

Layer / File(s) Summary
PAT creation and scope enforcement
src/controller/pat/*
Generates and refreshes secrets, validates expiry, computes project permissions, intersects requested scopes, and implements PAT lifecycle operations with tests.

PAT API contract and handlers

Layer / File(s) Summary
PAT API contract and handlers
api/v2.0/swagger.yaml, src/server/v2.0/handler/user.go, src/server/v2.0/handler/pat_test.go
Adds PAT OpenAPI operations and schemas, authenticated ownership checks, CRUD/list/refresh handlers, response mapping, audit logging, and handler coverage.

PAT hashing and authentication

Layer / File(s) Summary
PAT hashing and authentication
src/common/utils/encrypt.go, src/common/security/local/pat_context.go, src/server/middleware/security/*, make/migrations/postgresql/0202_2.17.0_widen_user_password.*
Adds versioned PAT hash verification, scoped security contexts, Basic Auth validation, expiry and disabled-state checks, and last-use tracking.

Legacy CLI secret migration

Layer / File(s) Summary
Legacy CLI secret migration
src/pkg/pat/migration/*, src/core/main.go
Migrates encrypted OIDC CLI secrets into idempotent legacy PAT records during startup and continues after per-record failures.

Persisted external-auth group membership

Layer / File(s) Summary
Persisted external-auth group membership
make/migrations/postgresql/0203_2.17.0_user_group_membership.*, src/pkg/usergroup/*, src/core/auth/ldap/ldap.go, src/pkg/oidc/helper.go, src/server/middleware/security/auth_proxy.go
Adds replace-and-list membership persistence and invokes best-effort synchronization from LDAP, OIDC, and auth-proxy authentication flows.

Portal PAT management

Layer / File(s) Summary
Portal PAT management
src/portal/src/app/base/*, src/portal/src/app/shared/components/navigator/*, src/portal/src/i18n/lang/*
Adds token listing, creation, scope selection, secret copying, refresh, revoke, deletion, modal navigation, supporting styles, component wiring, account-settings updates, and translations.

PAT integration scenarios

Layer / File(s) Summary
PAT integration scenarios
tests/robot-cases/Group1-Nightly/PAT.robot
Adds REST and registry scenarios covering token lifecycle, disabled tokens, usage tracking, self-management, and project-scoped access.

Sequence Diagram(s)

sequenceDiagram
  participant Portal
  participant UsersAPI
  participant PATController
  participant PATManager
  participant SecurityMiddleware
  Portal->>UsersAPI: Create or manage PAT
  UsersAPI->>PATController: Validate and process request
  PATController->>PATManager: Persist PAT metadata and hash
  PATController-->>UsersAPI: Return PAT response or secret
  SecurityMiddleware->>PATManager: Validate presented PAT
  SecurityMiddleware-->>Portal: Populate scoped security context
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Description check ⚠️ Warning The description covers the feature summary and testing plan, but it omits most required template sections like Type of Change, Release Notes, and Checklist. Add the missing template sections, especially Type of Change, Release Notes, Testing, and Checklist entries, or explicitly mark them as not applicable.
Docstring Coverage ⚠️ Warning Docstring coverage is 26.47% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title is concise and accurately summarizes the main change: adding PATs for CLI/API authentication.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
✨ Simplify code
  • Create PR with simplified code

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions github-actions Bot added documentation Improvements or additions to documentation component/portal component/api tests labels Jul 21, 2026
rossigee added a commit to rossigee/harbor-next-community that referenced this pull request Jul 21, 2026
Ports goharbor#287 for the harbor-next implementation
(container-registry/harbor-next#495).

Signed-off-by: Ross Golder <ross@golder.org>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 18

Note

Due to the large number of review comments, Critical, Major severity comments were prioritized as inline comments.

🟡 Minor comments (9)
src/pkg/pat/dao/dao_test.go-163-163 (1)

163-163: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Fix integer-to-string conversion in test data generation.

Using string(rune(i)) converts the integer i (which is 1, 2, or 3) into its corresponding Unicode control character (e.g., \x01, \x02), resulting in unprintable characters in the token name instead of the intended "token-1", "token-2", etc.

  • src/pkg/pat/dao/dao_test.go#L163-L163: Replace "token-" + string(rune(i)) with fmt.Sprintf("token-%d", i) or "token-" + strconv.Itoa(i).
  • src/pkg/pat/dao/dao_test.go#L183-L183: Apply the same fix here to generate correct token names.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/pkg/pat/dao/dao_test.go` at line 163, Fix integer-to-string conversion in
both token test-data generation sites: src/pkg/pat/dao/dao_test.go lines 163-163
and 183-183. Replace string(rune(i)) in the token name construction with a
decimal integer conversion such as fmt.Sprintf or strconv.Itoa so names are
generated as token-1, token-2, and token-3.
src/controller/pat/controller_test.go-41-46 (1)

41-46: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Avoid hardcoding user_id 1..12 here Harbor seeds the admin account with user_id = 1, so this range can collide with the default row and break SetupSuite. Use a non-reserved ID range or let the database assign IDs.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/controller/pat/controller_test.go` around lines 41 - 46, Update the test
user setup loop in SetupSuite to avoid explicitly inserting user_id values 1
through 12, which can collide with Harbor’s seeded admin account. Use a
non-reserved ID range or omit user_id so the database assigns IDs, while
preserving the generated usernames, emails, and real names.
src/server/middleware/security/pat_test.go-78-82 (1)

78-82: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Verify token usage timestamp update and security context.

The comment indicates "Verify user context", but the code retrieves a token without asserting any of its fields or the properties of secCtx. If this code is intended to verify the last_used_at timestamp update (as mentioned in the PR summary) or the generated security context fields, please add the missing assertions.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/server/middleware/security/pat_test.go` around lines 78 - 82, Strengthen
the relevant test in pat_test.go by asserting the retrieved token’s last_used_at
update and the generated secCtx security-context fields, rather than only
retrieving them. Use the existing token and security-context symbols in the
test, preserving its current setup while validating the expected values and user
context.
src/server/middleware/security/pat_test.go-304-308 (1)

304-308: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Assert the returned security context values.

Assigning the return values to the blank identifier (_ =) does not actually verify the correctness of the generated security context. Please replace these with actual assertions to ensure the context is populated correctly.

💚 Proposed fix
-	_ = secCtx.GetUsername()
-	_ = secCtx.IsAuthenticated()
-	_ = secCtx.IsSysAdmin()
-	_ = secCtx.IsSolutionUser()
-	_ = secCtx.Name()
+	suite.Equal("scopeuser", secCtx.GetUsername())
+	suite.True(secCtx.IsAuthenticated())
+	suite.False(secCtx.IsSysAdmin())
+	suite.False(secCtx.IsSolutionUser())
+	suite.Equal("pat", secCtx.Name())
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/server/middleware/security/pat_test.go` around lines 304 - 308, Update
the test around the security-context creation call in pat_test.go to capture its
returned values in named variables instead of blank identifiers, then assert
each value matches the expected populated security context. Preserve the
existing test setup and verify all returned context fields relevant to the
generated result.
src/server/v2.0/handler/user.go-75-80 (1)

75-80: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Missing nil checks for request bodies.

These endpoints directly dereference params.Request (and params.Request.Name). While framework-level OpenAPI validation often catches missing payloads, relying exclusively on it without programmatic nil checks poses a persistent crash risk (nil pointer dereference) if validation rules change.

Defensively verify that the request payload is not nil before accessing its properties:

  • src/server/v2.0/handler/user.go#L75-L80: In CreatePersonalAccessToken, add if params.Request == nil || params.Request.Name == nil { return u.SendError(ctx, errors.BadRequestError(nil).WithMessage("bad request")) }.
  • src/server/v2.0/handler/user.go#L204-L205: In RefreshPersonalAccessTokenSecret, check if params.Request == nil { return u.SendError(...) }.
  • src/server/v2.0/handler/user.go#L228-L237: In UpdatePersonalAccessToken, check if params.Request == nil { return u.SendError(...) }.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/server/v2.0/handler/user.go` around lines 75 - 80, Add defensive
request-body nil checks before dereferencing parameters: in
src/server/v2.0/handler/user.go lines 75-80 within CreatePersonalAccessToken,
reject nil Request or Name; in lines 204-205 within
RefreshPersonalAccessTokenSecret and lines 228-237 within
UpdatePersonalAccessToken, reject nil Request. Each validation should return the
existing bad-request SendError response.
src/server/v2.0/handler/user.go-670-686 (1)

670-686: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Include owner information in the audit description.

The 4th parameter _ string is ignored, despite being populated with fmt.Sprintf("user:%d", userID) by all callers. To ensure accurate and unambiguous audit trails (especially when an administrator manipulates another user's PAT), include this information in the OperationDescription.

📝 Proposed fix
-func (u *usersAPI) logAuditEntry(ctx context.Context, operation, patName, _ string, isSuccessful bool) {
+func (u *usersAPI) logAuditEntry(ctx context.Context, operation, patName, ownerInfo string, isSuccessful bool) {
 	sctx, _ := security.FromContext(ctx)
 	username := "unknown"
 	if sctx != nil {
 		username = sctx.GetUsername()
 	}
 
 	auditLog := &auditmodel.AuditLogExt{
 		Operation:            operation,
 		ResourceType:         "PAT",
 		Resource:             patName,
 		Username:             username,
 		OpTime:               time.Now(),
-		OperationDescription: fmt.Sprintf("%s personal access token %s", operation, patName),
+		OperationDescription: fmt.Sprintf("%s personal access token %s for %s", operation, patName, ownerInfo),
 		IsSuccessful:         isSuccessful,
 	}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/server/v2.0/handler/user.go` around lines 670 - 686, Update the affected
handler method in user.go to use its currently ignored fourth string parameter
when constructing OperationDescription, including the supplied owner value such
as “user:<id>”. Preserve the existing audit description content while appending
or incorporating this owner information so administrator actions on another
user’s PAT are unambiguous.
src/portal/src/app/base/account-settings/account-settings-modal.component.html-469-469 (1)

469-469: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Untranslated placeholder string.

placeholder="0 for never expire" is hardcoded English while every other user-facing string in this template uses | translate. This breaks localization for non-English users.

🌐 Proposed fix
                     [(ngModel)]="newPATForm.expiresInDays"
                     min="0"
-                    placeholder="0 for never expire" />
+                    [placeholder]="'PROFILE.PAT_EXPIRES_PLACEHOLDER' | translate" />

Add the corresponding key to the i18n resource files.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@src/portal/src/app/base/account-settings/account-settings-modal.component.html`
at line 469, Replace the hardcoded placeholder in the account settings modal
with a translation key using the template’s existing translate pattern, then add
the corresponding key and “0 for never expire” value to the project’s i18n
resource files for all supported locales.
src/portal/src/app/base/left-side-nav/system-robot-accounts/new-robot/new-robot.component.ts-117-117 (1)

117-117: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Fix the Prettier formatting error.

The single-line object type violates the repository’s formatting check and currently fails UI lint. Format its properties across multiple lines.

Proposed fix
-    secretValidation: { length: boolean; uppercase: boolean; lowercase: boolean; digit: boolean } = {
+    secretValidation: {
+        length: boolean;
+        uppercase: boolean;
+        lowercase: boolean;
+        digit: boolean;
+    } = {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@src/portal/src/app/base/left-side-nav/system-robot-accounts/new-robot/new-robot.component.ts`
at line 117, Update the single-line object type in the new robot component’s
type declaration to place its properties on separate lines, matching the
repository’s Prettier formatting rules and allowing the UI lint check to pass.

Source: Pipeline failures

src/portal/src/app/base/project/robot-account/add-robot/add-robot.component.ts-86-86 (1)

86-86: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Fix the Prettier formatting violation.

The inline secretValidation type fails the UI lint pipeline. Format the properties across multiple lines.

Proposed fix
-    secretValidation: { length: boolean; uppercase: boolean; lowercase: boolean; digit: boolean } = {
+    secretValidation: {
+        length: boolean;
+        uppercase: boolean;
+        lowercase: boolean;
+        digit: boolean;
+    } = {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@src/portal/src/app/base/project/robot-account/add-robot/add-robot.component.ts`
at line 86, Reformat the inline secretValidation type in the add-robot component
by placing its properties on separate lines, preserving the existing type
structure and behavior so the UI lint pipeline passes.

Source: Pipeline failures

🧹 Nitpick comments (25)
docs/OAUTH2_TOKEN_SUPPORT.md (1)

153-173: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick win

Expand the authentication test matrix.

Add coverage for expired, malformed, wrong-issuer, wrong-audience, invalid-signature, unknown-user, and path-restricted tokens, plus the compatibility case where AuthMode == OIDCAuth and the new flag is disabled.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/OAUTH2_TOKEN_SUPPORT.md` around lines 153 - 173, The Testing Strategy
section should expand its authentication matrix to cover expired, malformed,
wrong-issuer, wrong-audience, invalid-signature, unknown-user, and
path-restricted Bearer tokens, including their expected rejection behavior. Add
the compatibility case where AuthMode equals OIDCAuth while the new toggle is
disabled, and preserve the existing flag/AuthMode scenarios.
make/migrations/postgresql/0200_2.17.0_schema.up.sql (1)

19-19: 🚀 Performance & Scalability | 🔵 Trivial | 💤 Low value

Evaluate the usefulness of an index on a boolean column.

The disabled column is a boolean, which has very low cardinality (true/false). PostgreSQL's query optimizer typically ignores indexes on low-cardinality columns in favor of sequential scans, unless one value is extremely rare and the index is heavily used to filter for that rare value. Consider removing this index unless it significantly improves performance for specific query patterns.

♻️ Proposed refactor
-CREATE INDEX IF NOT EXISTS idx_pat_disabled ON personal_access_token (disabled);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@make/migrations/postgresql/0200_2.17.0_schema.up.sql` at line 19, Evaluate
the usefulness of idx_pat_disabled on the boolean personal_access_token.disabled
column and remove the index definition unless query patterns and benchmarking
demonstrate a meaningful performance benefit. Keep other migration changes
unchanged.
src/pkg/pat/dao/dao_test.go (1)

82-95: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Use the generated ID for retrieval instead of a hardcoded value.

Setting a hardcoded ID on the model before insertion can be misleading, as the database typically generates the primary key, and the ORM modifies the struct or returns the new ID. It is safer and more idiomatic to explicitly use the returned id for subsequent queries.

♻️ Proposed refactor
 	pat := &model.PersonalAccessToken{
-		ID:     1,
 		UserID: 3,
 		Name:   "get-test",
 		Secret: "secret",
 		Salt:   "salt",
 	}
 
 	// Create first
-	_, err := suite.dao.Create(suite.Context(), pat)
+	id, err := suite.dao.Create(suite.Context(), pat)
 	suite.NoError(err)
 
 	// Get
-	retrieved, err := suite.dao.Get(suite.Context(), pat.ID)
+	retrieved, err := suite.dao.Get(suite.Context(), id)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/pkg/pat/dao/dao_test.go` around lines 82 - 95, Update the Create/Get flow
in the test to capture the ID returned by suite.dao.Create, then pass that
generated ID to suite.dao.Get instead of relying on pat.ID or a hardcoded model
ID. Remove the preassigned ID if it is no longer needed.
src/pkg/pat/migration/migrate_cli_secrets.go (1)

80-87: 🚀 Performance & Scalability | 🔵 Trivial | 💤 Low value

Consider optimizing to avoid N+1 queries.

Calling patMgr.List inside the loop executes a database query for every OIDC user. If the instance has thousands of OIDC users, this can cause significant transaction delays.

If the personal_access_token table has a unique constraint on (user_id, name), you can skip this List check entirely and simply handle/ignore the duplicate constraint violation error returned by patMgr.Create. Alternatively, pre-fetch existing cli-secret PATs in a single query before the loop.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/pkg/pat/migration/migrate_cli_secrets.go` around lines 80 - 87, Remove
the per-user patMgr.List call from the OIDC-user migration loop to avoid N+1
database queries. Pre-fetch existing cli-secret personal access tokens once
before the loop, index them by user ID, and use that lookup to skip users who
already have one before calling patMgr.Create.
src/pkg/pat/migration/migrate_cli_secrets_test.go (2)

148-170: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Prefer fmt.Sprintf over rune arithmetic for string formatting.

Using string(rune(48+i)) for integer-to-string conversion is unidiomatic and harder to read. It's recommended to use fmt.Sprintf for constructing these test strings. Remember to add "fmt" to your imports if you apply this change.

♻️ Proposed refactor
 	// Create multiple test users
 	for i := 1; i <= 3; i++ {
 		u := &models.User{
-			Username: "user" + string(rune(48+i)),
-			Email:    "user" + string(rune(48+i)) + "`@example.com`",
-			Realname: "User " + string(rune(48+i)),
+			Username: fmt.Sprintf("user%d", i),
+			Email:    fmt.Sprintf("user%d@example.com", i),
+			Realname: fmt.Sprintf("User %d", i),
 		}
 		uid, err := suite.userCtl.Create(ctx, u)
 		suite.NoError(err)
 
 		// Create OIDC secret for each user
-		plainSecret := "secret-" + string(rune(48+i))
+		plainSecret := fmt.Sprintf("secret-%d", i)
 		secretKey, err := config.SecretKey()
 		suite.NoError(err)
 		encryptedSecret, err := utils.ReversibleEncrypt(plainSecret, secretKey)
 		suite.NoError(err)
 
 		oidcDAO := oidcdao.NewMetaDao()
 		oidcU := &models.OIDCUser{
 			UserID:  int(uid),
 			Secret:  encryptedSecret,
-			SubIss:  "sub" + string(rune(48+i)) + "|issuer",
-			Token:   "token" + string(rune(48+i)),
+			SubIss:  fmt.Sprintf("sub%d|issuer", i),
+			Token:   fmt.Sprintf("token%d", i),
 		}
 		_, err = oidcDAO.Create(ctx, oidcU)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/pkg/pat/migration/migrate_cli_secrets_test.go` around lines 148 - 170,
Replace the rune-arithmetic integer-to-string construction in the affected test
with fmt.Sprintf, adding the fmt import and preserving the existing generated
string values.

83-85: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Use suite.Require().Len to prevent test panics on failure.

suite.Equal does not halt the test upon failure. If the queried slice is empty during a failing test run, the test will proceed and panic with an "index out of range" error when accessing the first element. Using Require() ensures the test aborts cleanly if the length check fails.

  • src/pkg/pat/migration/migrate_cli_secrets_test.go#L83-L85: Change suite.Equal(1, len(pats)) to suite.Require().Len(pats, 1) to protect pats[0].
  • src/pkg/pat/migration/migrate_cli_secrets_test.go#L131-L132: Change suite.Equal(1, len(pats1)) to suite.Require().Len(pats1, 1) to protect downstream indexing of pats1.
  • src/pkg/pat/migration/migrate_cli_secrets_test.go#L140-L141: Change suite.Equal(1, len(pats2)) to suite.Require().Len(pats2, 1) to protect pats2[0].
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/pkg/pat/migration/migrate_cli_secrets_test.go` around lines 83 - 85,
Replace the non-fatal length assertions protecting indexed PAT slices with
require-style assertions: in src/pkg/pat/migration/migrate_cli_secrets_test.go
at lines 83-85, 131-132, and 140-141, update the checks for pats, pats1, and
pats2 to use suite.Require().Len with expected length 1 before their downstream
indexing.
src/common/security/local/pat_context.go (2)

110-114: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Dead code: init() only exists to reference otherwise-unused rbac constants.

_ = rbac.ActionPull, etc. serve no functional purpose here — HasPermission matches purely on strings (resourceStr/actionStr), never touching these constants. This reads as leftover debugging/import-satisfying code.

♻️ Proposed fix: remove dead init()
-
-func init() {
-	_ = rbac.ActionPull
-	_ = rbac.ActionPush
-	_ = rbac.ActionDelete
-}

(and drop the now-unused rbac import if nothing else in the file needs it)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/common/security/local/pat_context.go` around lines 110 - 114, Remove the
dead init function that only references rbac.ActionPull, rbac.ActionPush, and
rbac.ActionDelete, and remove the rbac import if no other code in the file uses
it. Leave HasPermission and its string-based permission matching unchanged.

36-45: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Scope parse failure is silently swallowed.

json.Unmarshal errors are discarded (_ = json.Unmarshal(...)), so a malformed scope string just results in an empty (fail-closed) scope with no diagnostic trail. Given this sits on the authentication/authorization hot path, logging the parse failure would materially help debug misconfigured or corrupted PAT scope data.

♻️ Proposed fix: log unmarshal failures
 	var parsedScope []patmodel.ProjectScope
 	if scope != "" {
-		_ = json.Unmarshal([]byte(scope), &parsedScope)
+		if err := json.Unmarshal([]byte(scope), &parsedScope); err != nil {
+			log.Warningf("failed to parse PAT scope for user %v: %v", user, err)
+		}
 	}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/common/security/local/pat_context.go` around lines 36 - 45, Update
NewPATSecurityContext to capture the error returned by json.Unmarshal when
parsing a non-empty scope and log the failure with the relevant diagnostic
context instead of discarding it. Preserve the existing parsedScope behavior and
fail-closed result for malformed scope data.
src/server/middleware/security/pat_test.go (1)

270-272: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Move the test suite runner to the end of the file.

Conventionally, the test suite runner function (TestPATSecurityTestSuite) is placed at the very end of the file, after all the suite methods have been defined.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/server/middleware/security/pat_test.go` around lines 270 - 272, Move the
TestPATSecurityTestSuite function to the end of the file, after all PAT security
test suite methods, without changing its behavior or the surrounding tests.
api/v2.0/swagger.yaml (1)

6218-6222: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Add a 400 Bad Request response for invalid payloads.

The PATCH endpoint accepts a body (PersonalAccessTokenRefreshRequest). If the client provides malformed JSON or an invalid secret format, the server will likely return a 400 Bad Request. Documenting this response ensures API clients can handle validation errors properly.

♻️ Proposed refactor
         '200':
           description: Personal access token secret refreshed successfully.
           schema:
             $ref: '`#/definitions/PersonalAccessTokenCreatedResponse`'
+        '400':
+          $ref: '`#/responses/400`'
         '401':
           $ref: '`#/responses/401`'
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@api/v2.0/swagger.yaml` around lines 6218 - 6222, Update the PATCH endpoint
response definitions near the existing 200 and 401 responses to include a 400
Bad Request response for malformed JSON or invalid
PersonalAccessTokenRefreshRequest payloads, with a clear description and the
appropriate error schema used by the API.
src/server/v2.0/handler/pat_test.go (1)

54-54: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Use Require().NoError to fail fast on setup errors.

If user creation fails, continuing execution will cause uid to be 0, leading to confusing downstream test failures. Require().NoError(err) stops the test execution immediately. Consider applying this pattern throughout the test suite wherever subsequent assertions depend on the success of an operation.

♻️ Proposed refactor
-	suite.NoError(err)
+	suite.Require().NoError(err)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/server/v2.0/handler/pat_test.go` at line 54, Update the user-creation
setup in the affected test to use the test suite’s Require().NoError(err)
assertion instead of a non-fatal error check, so execution stops before using an
invalid uid. Apply the same fail-fast pattern to nearby setup operations whose
results are required by subsequent assertions.
src/portal/src/app/base/account-settings/account-settings-modal.component.html (2)

401-431: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Use @else for the mutually exclusive PAT states.

Lines 401 (@if (createdPATSecret)) and 431 (@if (!createdPATSecret)) express one boolean branch as two separate @if blocks that each re-evaluate the condition. The footer (Line 566) shares the same pattern. Collapsing these into @if (createdPATSecret) { ... } @else { ... } is clearer and avoids duplicate condition evaluation.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@src/portal/src/app/base/account-settings/account-settings-modal.component.html`
around lines 401 - 431, In the account settings modal template, replace the
mutually exclusive PAT-state `@if (createdPATSecret)` and `@if
(!createdPATSecret)` blocks with a single `@if (createdPATSecret) { ... } `@else`
{ ... }` structure. Apply the same conversion to the footer’s corresponding
PAT-state conditions, preserving each branch’s existing content.

525-543: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Track by a stable key and add accessible labels to scope checkboxes.

Two suggestions for the scope table:

  • track p tracks by object identity; prefer track p.project_id so @for diffing stays stable and cheap.
  • The pull/push checkboxes (Lines 529-540) have no associated <label> or aria-label, so screen readers announce them without project/action context.
♻️ Proposed changes
-                            `@for` (p of scopeProjects; track p) {
+                            `@for` (p of scopeProjects; track p.project_id) {
                             <tr>
                                 <td>{{ p.project_name }}</td>
                                 <td style="text-align: center">
                                     <input
                                         clrCheckbox
                                         type="checkbox"
                                         [(ngModel)]="p.pull"
-                                        [name]="'pull_' + p.project_id" />
+                                        [name]="'pull_' + p.project_id"
+                                        [attr.aria-label]="('PROFILE.PAT_SCOPE_PULL' | translate) + ' ' + p.project_name" />
                                 </td>
                                 <td style="text-align: center">
                                     <input
                                         clrCheckbox
                                         type="checkbox"
                                         [(ngModel)]="p.push"
-                                        [name]="'push_' + p.project_id" />
+                                        [name]="'push_' + p.project_id"
+                                        [attr.aria-label]="('PROFILE.PAT_SCOPE_PUSH' | translate) + ' ' + p.project_name" />
                                 </td>
                             </tr>
                             }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@src/portal/src/app/base/account-settings/account-settings-modal.component.html`
around lines 525 - 543, Update the scope table’s `@for` block to track each
project by `p.project_id` instead of object identity, and add accessible labels
to the pull and push checkboxes so each conveys both the project and action
context, using associated labels or appropriate `aria-label` attributes.
src/portal/src/app/base/account-settings/account-settings-modal.component.ts (3)

653-657: 🩺 Stability & Availability | 🔵 Trivial | 💤 Low value

Handle clipboard write failure.

navigator.clipboard.writeText returns a promise that can reject (e.g. permission denied, insecure context, or navigator.clipboard being undefined over plain HTTP). Currently a failed copy is silent, leaving the user unaware. Consider adding a .then/.catch to surface success/failure feedback.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/portal/src/app/base/account-settings/account-settings-modal.component.ts`
around lines 653 - 657, Update the clipboard-copy flow in
account-settings-modal.component.ts to handle navigator.clipboard.writeText
failures, including unavailable clipboard APIs and rejected promises. Surface
clear success feedback on completion and failure feedback when the write cannot
be performed, while preserving the existing copy behavior.

753-762: 🎯 Functional Correctness | 🔵 Trivial | 💤 Low value

Fragile fallback in confirmAction.

Any confirmed message whose source is not USER_PAT falls through to confirmGenerate() and generates a new CLI secret. If this shared confirmation dialog ever handles another target in this component, an unrelated confirmation would unexpectedly regenerate the CLI secret. Prefer matching the generate target explicitly rather than using it as the catch-all else.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/portal/src/app/base/account-settings/account-settings-modal.component.ts`
around lines 753 - 762, Update confirmAction so confirmGenerate is invoked only
when the confirmed message explicitly targets CLI secret generation, rather than
using a non-USER_PAT source as the fallback condition. Ensure confirmations for
unrelated targets do not generate a new secret, while preserving the existing
USER_PAT behavior.

91-99: 🎯 Functional Correctness | 🔵 Trivial | 💤 Low value

Empty scopeProjects makes both getters true.

Array.prototype.every returns true on an empty array, so when no projects are loaded both allScopeSelected and noScopeSelected report true. This is mostly benign here because buildScopeJson treats "all selected" as full auto-compute, but any template logic that relies on these two being mutually exclusive will misbehave for users with zero projects. Consider short-circuiting on scopeProjects.length === 0.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/portal/src/app/base/account-settings/account-settings-modal.component.ts`
around lines 91 - 99, Update the allScopeSelected and noScopeSelected getters to
explicitly handle an empty scopeProjects array before calling every. Return a
mutually exclusive, appropriate false state when no projects are loaded, while
preserving the existing selection checks for non-empty project lists.
src/portal/src/app/base/account-settings/api-tokens-modal.component.html (4)

20-22: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Remove row selection if batch actions are unsupported.

Binding [(clrDgSelected)]="selectedTokens" automatically adds row selection checkboxes to the datagrid. Since there are no batch actions (e.g., "Delete Selected") in the token toolbar, these checkboxes serve no purpose and can confuse users.

If bulk actions are not planned, remove this binding to hide the checkboxes.

♻️ Proposed refactor
-            <clr-datagrid
-                [(clrDgSelected)]="selectedTokens"
-                [clrDgLoading]="tokenLoading">
+            <clr-datagrid
+                [clrDgLoading]="tokenLoading">
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/portal/src/app/base/account-settings/api-tokens-modal.component.html`
around lines 20 - 22, Remove the [(clrDgSelected)]="selectedTokens" binding from
the token datagrid in the account tokens modal so row-selection checkboxes are
not rendered when no batch actions are available.

129-139: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Improve form accessibility by handling submission natively.

The form relies exclusively on a button click event to trigger submission. This breaks native HTML accessibility, preventing users from submitting the form by pressing the Enter key.

Bind the (ngSubmit) event directly to the <form> element. Additionally, update the "Create" button (line 205) to type="submit" and remove its (click) binding so it seamlessly hooks into the form.

♻️ Proposed form tag update
-        <form>
+        <form (ngSubmit)="createToken()">
             <clr-input-container>
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/portal/src/app/base/account-settings/api-tokens-modal.component.html`
around lines 129 - 139, Update the form in
account-settings/api-tokens-modal.component.html to bind its (ngSubmit) event to
the existing submission handler, allowing native Enter-key submission. Change
the “Create” button to type="submit" and remove its (click) binding so
submission is handled exclusively by the form.

126-131: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Standardize translation mechanisms.

This template mixes Angular's native i18n="@@..." attributes (used here on lines 126 and 131) with @ngx-translate's translate pipe (used elsewhere in this file, such as lines 16 and 25).

To simplify translation extraction and maintenance, it is recommended to standardize on a single translation mechanism—typically the translate pipe across Harbor UI components.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/portal/src/app/base/account-settings/api-tokens-modal.component.html`
around lines 126 - 131, Standardize the translation approach in the
account-tokens modal template by replacing the native Angular i18n attributes on
the affected elements with the existing `@ngx-translate` translate pipe pattern
used elsewhere in the template. Preserve the current translation keys and
rendered text while ensuring all translatable content uses the pipe
consistently.

132-148: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Disable inputs when the token secret is displayed.

When createdTokenSecret is populated, the modal displays the newly generated token secret. However, the Name and Description inputs remain editable, which might lead users to believe they can still update the newly created token's details.

Consider disabling these inputs once the token is created to clarify the state.

♻️ Proposed refactor
                 <input
                     clrInput
                     type="text"
                     placeholder="Enter token name"
                     [(ngModel)]="newTokenForm.name"
                     name="token_name"
+                    [disabled]="!!createdTokenSecret"
                     required />
             </clr-input-container>
 
             <clr-textarea-container>
                 <label i18n="@@PROFILE.DESCRIPTION">Description</label>
                 <textarea
                     clrTextarea
                     placeholder="Enter token description (optional)"
                     [(ngModel)]="newTokenForm.description"
+                    [disabled]="!!createdTokenSecret"
                     name="token_description"></textarea>
             </clr-textarea-container>
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/portal/src/app/base/account-settings/api-tokens-modal.component.html`
around lines 132 - 148, Update the Name and Description inputs in the account
token modal to be disabled whenever createdTokenSecret is populated, while
keeping them editable before token creation. Preserve the existing token-secret
display behavior and bindings.
tests/robot-cases/Group1-Nightly/PAT.robot (3)

234-234: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

[Return] setting is deprecated.

Robot Framework deprecated [Return] in favor of the RETURN statement starting in RF 5.0, with visible deprecation warnings since RF 7.0.

Also applies to: 245-245, 270-270

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/robot-cases/Group1-Nightly/PAT.robot` at line 234, Replace the
deprecated [Return] settings in the affected Robot Framework keywords with the
modern RETURN statement. Update all occurrences at the referenced locations
while preserving each keyword’s existing return values and control flow.

207-211: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Test Case 12 is a no-op that always passes.

The body only logs messages and performs no API calls or assertions, so it provides no actual coverage of OIDC auto-onboarding despite the documentation claim. Consider tagging it [Skip]/excluding it from the suite (e.g. via a Skip keyword or exclusion tag) rather than leaving a test case that trivially "passes" with zero verification.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/robot-cases/Group1-Nightly/PAT.robot` around lines 207 - 211, Update
the Test Case 12 definition in PAT.robot to exclude it from execution, using the
suite’s established Skip keyword or exclusion tag mechanism. Do not leave the
no-op logging body as an active test that reports success without API calls or
assertions.

102-103: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Hardcoded project names lack the uniqueness suffix used elsewhere.

test-project-docker, scoped-project-1, and scoped-project-2 are static, unlike other tests in this suite that append ${d} to avoid collisions across repeated/parallel runs. If a prior run's cleanup (Lines 124, 203-204) fails or is skipped, subsequent runs will collide on Create Project.

Also applies to: 179-183

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/robot-cases/Group1-Nightly/PAT.robot` around lines 102 - 103, Update
the project names used by the affected tests, including the Create Project flow
and the references around the cleanup sections, to append the existing ${d}
uniqueness suffix. Apply it consistently to test-project-docker,
scoped-project-1, and scoped-project-2 while preserving matching references
within each test.
src/portal/src/app/base/account-settings/api-tokens-modal.component.ts (1)

100-108: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Replace deprecated document.execCommand('copy').

The document.execCommand API is deprecated. Consider using the modern Clipboard API (navigator.clipboard.writeText) or Angular CDK's clipboard service if it is already available in your dependencies.

♻️ Proposed fix using Clipboard API
     copyTokenSecret(): void {
-        const copyInput = document.createElement('textarea');
-        copyInput.value = this.createdTokenSecret;
-        document.body.appendChild(copyInput);
-        copyInput.select();
-        document.execCommand('copy');
-        document.body.removeChild(copyInput);
-        this.msgHandler.showSuccess('Token copied to clipboard');
+        if (navigator.clipboard) {
+            navigator.clipboard.writeText(this.createdTokenSecret).then(() => {
+                this.msgHandler.showSuccess('Token copied to clipboard');
+            }).catch(() => {
+                this.msgHandler.showError('Failed to copy token');
+            });
+        }
     }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/portal/src/app/base/account-settings/api-tokens-modal.component.ts`
around lines 100 - 108, Replace the deprecated document.execCommand('copy')
usage in the API token copy flow with navigator.clipboard.writeText, or reuse
Angular CDK’s clipboard service if already available. Preserve the existing
copied-content and success/error behavior while handling the asynchronous
clipboard operation.
src/portal/src/app/shared/components/navigator/navigator.component.html (1)

89-94: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick win

Avoid the javascript: URL.

This item only opens a modal, so use a button-style dropdown item or another non-script URL. javascript:void(0) can violate CSP policies and is unnecessary when Angular already handles the click.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/portal/src/app/shared/components/navigator/navigator.component.html`
around lines 89 - 94, Replace the navigator dropdown item’s javascript: URL with
a button-style dropdown item or another non-script target while preserving the
existing Angular click handler and modal behavior. Remove the unnecessary
javascript:void(0) usage from the affected template element.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: d56fd8ef-ea94-46d3-847f-700437a90910

📥 Commits

Reviewing files that changed from the base of the PR and between bf895ae and 2b71504.

📒 Files selected for processing (41)
  • api/v2.0/swagger.yaml
  • docs/OAUTH2_TOKEN_SUPPORT.md
  • make/migrations/postgresql/0200_2.17.0_schema.up.sql
  • make/migrations/postgresql/0201_2.17.0_pat_scope.down.sql
  • make/migrations/postgresql/0201_2.17.0_pat_scope.up.sql
  • make/photon/core/harbor_core
  • make/photon/core/harbor_core_new
  • src/common/security/local/pat_context.go
  • src/common/security/local/pat_context_test.go
  • src/controller/pat/controller.go
  • src/controller/pat/controller_test.go
  • src/controller/user/controller_link_test.go
  • src/pkg/pat/dao/dao.go
  • src/pkg/pat/dao/dao_test.go
  • src/pkg/pat/manager.go
  • src/pkg/pat/migration/migrate_cli_secrets.go
  • src/pkg/pat/migration/migrate_cli_secrets_test.go
  • src/pkg/pat/model/model.go
  • src/pkg/pat/model/model_test.go
  • src/portal/src/app/base/account-settings/account-settings-modal.component.html
  • src/portal/src/app/base/account-settings/account-settings-modal.component.spec.ts
  • src/portal/src/app/base/account-settings/account-settings-modal.component.ts
  • src/portal/src/app/base/account-settings/api-tokens-modal.component.html
  • src/portal/src/app/base/account-settings/api-tokens-modal.component.scss
  • src/portal/src/app/base/account-settings/api-tokens-modal.component.ts
  • src/portal/src/app/base/base.module.ts
  • src/portal/src/app/base/harbor-shell/harbor-shell.component.html
  • src/portal/src/app/base/harbor-shell/harbor-shell.component.spec.ts
  • src/portal/src/app/base/harbor-shell/harbor-shell.component.ts
  • src/portal/src/app/base/left-side-nav/system-robot-accounts/new-robot/new-robot.component.ts
  • src/portal/src/app/base/modal-events.const.ts
  • src/portal/src/app/base/project/robot-account/add-robot/add-robot.component.ts
  • src/portal/src/app/shared/components/navigator/navigator.component.html
  • src/portal/src/app/shared/components/navigator/navigator.component.ts
  • src/portal/src/app/shared/entities/shared.const.ts
  • src/server/middleware/security/pat.go
  • src/server/middleware/security/pat_test.go
  • src/server/v2.0/handler/pat.go
  • src/server/v2.0/handler/pat_test.go
  • src/server/v2.0/handler/user.go
  • tests/robot-cases/Group1-Nightly/PAT.robot

Comment thread docs/OAUTH2_TOKEN_SUPPORT.md Outdated
Comment thread src/controller/pat/controller.go
Comment thread src/pkg/pat/migration/migrate_cli_secrets.go
Comment thread src/portal/src/app/base/account-settings/account-settings-modal.component.ts Outdated
Comment thread tests/robot-cases/Group1-Nightly/PAT.robot
Comment thread tests/robot-cases/Group1-Nightly/PAT.robot
Comment thread tests/robot-cases/Group1-Nightly/PAT.robot Outdated
Comment thread tests/robot-cases/Group1-Nightly/PAT.robot
Comment thread tests/robot-cases/Group1-Nightly/PAT.robot

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All reported issues were addressed across 41 files

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread src/server/middleware/security/pat.go
Comment thread src/portal/src/app/base/account-settings/account-settings-modal.component.html Outdated
Comment thread src/server/v2.0/handler/pat_test.go Outdated
Comment thread tests/robot-cases/Group1-Nightly/PAT.robot Outdated
Comment thread src/pkg/pat/migration/migrate_cli_secrets_test.go Outdated
Comment thread src/portal/src/app/base/project/robot-account/add-robot/add-robot.component.ts Outdated
Comment thread src/server/v2.0/handler/pat.go Outdated
Comment thread src/controller/user/controller_link_test.go Outdated
Comment thread src/controller/pat/controller_test.go
Comment thread src/portal/src/app/base/account-settings/account-settings-modal.component.html Outdated

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All reported issues were addressed across 20 files (changes from recent commits).

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread src/server/middleware/security/pat_test.go Outdated
Comment thread src/portal/src/i18n/lang/zh-tw-lang.json Outdated
@github-actions github-actions Bot removed the documentation Improvements or additions to documentation label Jul 21, 2026

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All reported issues were addressed across 5 files (changes from recent commits).

Tip: Review your code locally with the cubic CLI to iterate faster.

Re-trigger cubic

Comment thread src/controller/pat/controller.go Outdated
Comment thread src/portal/src/app/base/account-settings/account-settings-modal.component.ts Outdated

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All reported issues were addressed across 3 files (changes from recent commits).

Tip: Review your code locally with the cubic CLI to iterate faster.

Re-trigger cubic

Comment thread src/server/middleware/security/pat.go Outdated

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All reported issues were addressed across 19 files (changes from recent commits).

Tip: Review your code locally with the cubic CLI to iterate faster.

Re-trigger cubic

Comment thread src/core/main.go Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 8

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
src/pkg/pat/migration/migrate_cli_secrets.go (2)

79-90: 🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Make the idempotency check atomic.

The list-then-create sequence is vulnerable to a TOCTOU race. Because src/core/main.go invokes this migration during every core startup, concurrent instances can both observe no PAT and create duplicates. Enforce a database uniqueness constraint and use an insert-on-conflict/upsert path, treating duplicate-key errors as skips.

Also applies to: 116-120

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/pkg/pat/migration/migrate_cli_secrets.go` around lines 79 - 90, Make the
cli-secret migration idempotent under concurrent startups by adding a database
uniqueness constraint for the user and PAT name, then replacing the
list-then-create flow around patMgr.List with an insert-on-conflict or upsert
operation. Treat duplicate-key conflicts as skipped users, while preserving
existing handling for other errors and successful creations; apply the same
change to the corresponding later creation path.

79-90: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Reserve cli-secret end-to-end. IsLegacy alone won’t disambiguate here: personal_access_token is still unique on (user_id, name), and there’s no PAT-name reservation/validation elsewhere, so a pre-existing user PAT with this name will skip the migration and leave that user’s OIDC secret unmigrated. Add a collision test for a non-legacy cli-secret PAT.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/pkg/pat/migration/migrate_cli_secrets.go` around lines 79 - 90, The
migration’s existing-name check incorrectly treats any user PAT named
“cli-secret” as the reserved migration token, allowing a non-legacy token to
block OIDC secret migration. Update the migration logic and PAT name
validation/reservation path around the migration entry point and PAT creation
symbols so “cli-secret” is reserved end-to-end, while distinguishing or handling
non-legacy collisions explicitly. Add a test covering a user with a non-legacy
“cli-secret” PAT and verify the OIDC secret is still migrated as required.
🧹 Nitpick comments (4)
src/common/security/local/pat_context.go (1)

36-42: 🩺 Stability & Availability | 🔵 Trivial | 💤 Low value

Guard against nil pointer dereference on user.

While the current middleware flow guarantees user is not nil, NewPATSecurityContext is an exported constructor. If it is ever called with a nil user (e.g., in unit tests or future authentication flows), user.Username will panic when scope parsing fails. Consider adding a defensive check.

🛡️ Proposed defensive fix
 func NewPATSecurityContext(user *models.User, scope string) *PATSecurityContext {
 	var parsedScope []patmodel.ProjectScope
 	if scope != "" {
 		if err := json.Unmarshal([]byte(scope), &parsedScope); err != nil {
-			log.Warningf("failed to parse PAT scope for user %v: %v", user.Username, err)
+			username := "unknown"
+			if user != nil {
+				username = user.Username
+			}
+			log.Warningf("failed to parse PAT scope for user %v: %v", username, err)
 		}
 	}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/common/security/local/pat_context.go` around lines 36 - 42, Update
NewPATSecurityContext to safely handle a nil user when JSON scope parsing fails;
avoid dereferencing user.Username unless user is non-nil, while preserving the
existing warning behavior and parsed-scope handling for valid users.
src/portal/src/app/base/account-settings/api-tokens-modal.component.ts (1)

267-275: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

document.execCommand('copy') is deprecated; prefer the Clipboard API.

Modern browsers support navigator.clipboard.writeText(), which is the recommended replacement for the deprecated execCommand('copy') approach used here and in copyRefreshedTokenSecret/copyTokenSecret.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/portal/src/app/base/account-settings/api-tokens-modal.component.ts`
around lines 267 - 275, Update copySecretToClipboard and the related
copyRefreshedTokenSecret and copyTokenSecret flows to use
navigator.clipboard.writeText() instead of the deprecated
document.execCommand('copy') textarea approach. Preserve the existing success
notification and ensure the asynchronous clipboard operation is handled
correctly.
src/portal/src/app/base/account-settings/api-tokens-modal.component.html (2)

167-235: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Prefer CSS classes over inline styles for the new scope-selection UI.

This block relies entirely on inline style="..." attributes (margin, font-size, color, width) instead of classes in the sibling api-tokens-modal.component.scss, which was added specifically for this component.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/portal/src/app/base/account-settings/api-tokens-modal.component.html`
around lines 167 - 235, The new scope-selection markup in the scope-section
block should use CSS classes instead of inline style attributes. Add or reuse
descriptive classes in api-tokens-modal.component.scss for spacing, typography,
colors, table column widths, and alignment, then replace the corresponding
inline styles while preserving the current layout and appearance.

92-96: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Refreshing a secret is destructive but has no confirmation step.

Clicking "Refresh" immediately invalidates the old secret (any script/CI using it breaks) with no confirmation dialog, unlike typical destructive-action UX patterns elsewhere in Harbor.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/portal/src/app/base/account-settings/api-tokens-modal.component.html`
around lines 92 - 96, Add a confirmation step to the refresh action wired
through refreshTokenSecret before invalidating the existing token secret, using
the application’s established destructive-action confirmation pattern and
proceeding only when the user confirms; keep the existing refresh behavior
unchanged after confirmation.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/common/utils/encrypt.go`:
- Around line 111-127: Update VerifyPATHash to split the versioned hash into and
validate exactly the expected number of segments before indexing, returning
false when the salt/hash separator is missing or extra segments are present.
Handle strconv.Atoi errors and reject non-positive iteration counts before
calling pbkdf2.Key, while preserving the existing legacy-hash verification path.
- Around line 96-109: Restore Encrypt’s legacy plain-hex output for existing
password checks and the PAT fallback path instead of delegating to
EncryptWithIterations. Keep the prefixed format in EncryptWithIterations, and
update only callers that require the PAT format to invoke EncryptWithIterations
explicitly.

In `@src/core/main.go`:
- Around line 294-297: Update the startup migration flow around
patmigration.MigrateCliSecretsToLegacyPATs so global migration failures are
propagated as startup failures or trigger a retry instead of being logged with
log.Warningf and ignored. Preserve the nonfatal handling of per-user migration
errors, while ensuring failures obtaining the ORM, querying oidc_user, or
loading the secret key prevent the migration from being treated as complete.

In `@src/portal/src/app/base/account-settings/api-tokens-modal.component.ts`:
- Around line 156-186: Update buildScopeJson() so an explicit deselection
returns a serialized empty scope array instead of undefined when selected.length
=== 0. Preserve the existing undefined result for allScopeSelected, allowing
only that intentional state to trigger backend auto-computation, while ensuring
deselectAllScope() produces no access.
- Around line 36-41: Map the default expiresInDays value of 0 to the API’s
never-expire value of -1 in newTokenForm and ensure the submit/create path
around lines 188-224 never forwards 0 as expires_in_days; update
tests/robot-cases/Group1-Nightly/PAT.robot lines 264-274 to verify the
never-expire behavior.

In `@src/portal/src/i18n/lang/ru-ru-lang.json`:
- Line 301: Move PAT_EXPIRES_PLACEHOLDER from SIDE_NAV.PROFILE into the root
PROFILE namespace alongside the other PROFILE translation keys, preserving its
existing Russian value and removing the nested duplicate.

In `@src/portal/src/i18n/lang/tr-tr-lang.json`:
- Line 128: Update the PAT_EXPIRES_PLACEHOLDER translation to natural Turkish,
replacing the English “expires” wording with a clear phrase conveying that 0
means the token never expires.

In `@tests/robot-cases/Group1-Nightly/PAT.robot`:
- Around line 264-274: The PAT creation requests in Create PAT Via API, Create
PAT For User, and Test Case 9 use the wrong expiration field and value. Replace
expires_at with expires_in_days and pass ${expiry_days}, using -1 when
expiration is disabled; apply this consistently to all three PAT creation calls.

---

Outside diff comments:
In `@src/pkg/pat/migration/migrate_cli_secrets.go`:
- Around line 79-90: Make the cli-secret migration idempotent under concurrent
startups by adding a database uniqueness constraint for the user and PAT name,
then replacing the list-then-create flow around patMgr.List with an
insert-on-conflict or upsert operation. Treat duplicate-key conflicts as skipped
users, while preserving existing handling for other errors and successful
creations; apply the same change to the corresponding later creation path.
- Around line 79-90: The migration’s existing-name check incorrectly treats any
user PAT named “cli-secret” as the reserved migration token, allowing a
non-legacy token to block OIDC secret migration. Update the migration logic and
PAT name validation/reservation path around the migration entry point and PAT
creation symbols so “cli-secret” is reserved end-to-end, while distinguishing or
handling non-legacy collisions explicitly. Add a test covering a user with a
non-legacy “cli-secret” PAT and verify the OIDC secret is still migrated as
required.

---

Nitpick comments:
In `@src/common/security/local/pat_context.go`:
- Around line 36-42: Update NewPATSecurityContext to safely handle a nil user
when JSON scope parsing fails; avoid dereferencing user.Username unless user is
non-nil, while preserving the existing warning behavior and parsed-scope
handling for valid users.

In `@src/portal/src/app/base/account-settings/api-tokens-modal.component.html`:
- Around line 167-235: The new scope-selection markup in the scope-section block
should use CSS classes instead of inline style attributes. Add or reuse
descriptive classes in api-tokens-modal.component.scss for spacing, typography,
colors, table column widths, and alignment, then replace the corresponding
inline styles while preserving the current layout and appearance.
- Around line 92-96: Add a confirmation step to the refresh action wired through
refreshTokenSecret before invalidating the existing token secret, using the
application’s established destructive-action confirmation pattern and proceeding
only when the user confirms; keep the existing refresh behavior unchanged after
confirmation.

In `@src/portal/src/app/base/account-settings/api-tokens-modal.component.ts`:
- Around line 267-275: Update copySecretToClipboard and the related
copyRefreshedTokenSecret and copyTokenSecret flows to use
navigator.clipboard.writeText() instead of the deprecated
document.execCommand('copy') textarea approach. Preserve the existing success
notification and ensure the asynchronous clipboard operation is handled
correctly.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: fbc679b0-d4a0-42b5-8e0b-b3a4f38ee8d8

📥 Commits

Reviewing files that changed from the base of the PR and between 2b71504 and cc81161.

📒 Files selected for processing (32)
  • api/v2.0/swagger.yaml
  • make/migrations/postgresql/0200_2.17.0_schema.up.sql
  • src/common/security/local/pat_context.go
  • src/common/utils/encrypt.go
  • src/controller/pat/controller.go
  • src/controller/pat/controller_test.go
  • src/core/main.go
  • src/pkg/pat/dao/dao_test.go
  • src/pkg/pat/migration/migrate_cli_secrets.go
  • src/pkg/pat/migration/migrate_cli_secrets_test.go
  • src/portal/src/app/base/account-settings/account-settings-modal.component.html
  • src/portal/src/app/base/account-settings/account-settings-modal.component.spec.ts
  • src/portal/src/app/base/account-settings/account-settings-modal.component.ts
  • src/portal/src/app/base/account-settings/api-tokens-modal.component.html
  • src/portal/src/app/base/account-settings/api-tokens-modal.component.ts
  • src/portal/src/app/base/left-side-nav/system-robot-accounts/new-robot/new-robot.component.ts
  • src/portal/src/i18n/lang/de-de-lang.json
  • src/portal/src/i18n/lang/en-us-lang.json
  • src/portal/src/i18n/lang/es-es-lang.json
  • src/portal/src/i18n/lang/fr-fr-lang.json
  • src/portal/src/i18n/lang/ko-kr-lang.json
  • src/portal/src/i18n/lang/pt-br-lang.json
  • src/portal/src/i18n/lang/ru-ru-lang.json
  • src/portal/src/i18n/lang/tr-tr-lang.json
  • src/portal/src/i18n/lang/zh-cn-lang.json
  • src/portal/src/i18n/lang/zh-tw-lang.json
  • src/server/middleware/security/pat.go
  • src/server/middleware/security/pat_test.go
  • src/server/middleware/security/security.go
  • src/server/v2.0/handler/pat_test.go
  • src/server/v2.0/handler/user.go
  • tests/robot-cases/Group1-Nightly/PAT.robot
💤 Files with no reviewable changes (1)
  • src/portal/src/app/base/account-settings/account-settings-modal.component.spec.ts
🚧 Files skipped from review as they are similar to previous changes (11)
  • src/server/middleware/security/pat.go
  • src/portal/src/app/base/left-side-nav/system-robot-accounts/new-robot/new-robot.component.ts
  • make/migrations/postgresql/0200_2.17.0_schema.up.sql
  • src/server/v2.0/handler/pat_test.go
  • src/pkg/pat/migration/migrate_cli_secrets_test.go
  • src/pkg/pat/dao/dao_test.go
  • api/v2.0/swagger.yaml
  • src/controller/pat/controller_test.go
  • src/server/middleware/security/pat_test.go
  • src/controller/pat/controller.go
  • src/server/v2.0/handler/user.go

Comment thread src/common/utils/encrypt.go Outdated
Comment thread src/common/utils/encrypt.go Outdated
Comment thread src/core/main.go Outdated
Comment thread src/portal/src/i18n/lang/ru-ru-lang.json Outdated
Comment thread src/portal/src/i18n/lang/tr-tr-lang.json Outdated
Comment thread tests/robot-cases/Group1-Nightly/PAT.robot

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All reported issues were addressed across 7 files (changes from recent commits).

Tip: Review your code locally with the cubic CLI to iterate faster.

Re-trigger cubic

Comment thread src/portal/src/app/base/account-settings/api-tokens-modal.component.ts Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
make/migrations/postgresql/0202_2.17.0_widen_user_password.up.sql (1)

6-6: 🩺 Stability & Availability | 🔵 Trivial

Plan this lock-taking migration outside peak traffic.

ALTER TABLE requires an ACCESS EXCLUSIVE lock; if startup migration waits behind long-running transactions, it can block reads and writes. Run or schedule it during a controlled deployment window and monitor lock acquisition.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@make/migrations/postgresql/0202_2.17.0_widen_user_password.up.sql` at line 6,
Plan the password column widening migration represented by the ALTER TABLE
statement for a controlled, off-peak deployment window. Ensure startup migration
execution is scheduled to avoid long-running transactions, and monitor lock
acquisition while it runs.

Source: Linters/SAST tools

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@make/migrations/postgresql/0202_2.17.0_widen_user_password.down.sql`:
- Line 1: Update the rollback migration for harbor_user.password so it does not
narrow the column back to varchar(40); retain the wider column type introduced
by the upgrade, or explicitly make the migration irreversible.

---

Nitpick comments:
In `@make/migrations/postgresql/0202_2.17.0_widen_user_password.up.sql`:
- Line 6: Plan the password column widening migration represented by the ALTER
TABLE statement for a controlled, off-peak deployment window. Ensure startup
migration execution is scheduled to avoid long-running transactions, and monitor
lock acquisition while it runs.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 5e4245c5-b80f-4f30-a1bf-5de4bc6c7ba8

📥 Commits

Reviewing files that changed from the base of the PR and between cc81161 and d3f1d63.

📒 Files selected for processing (11)
  • make/migrations/postgresql/0202_2.17.0_widen_user_password.down.sql
  • make/migrations/postgresql/0202_2.17.0_widen_user_password.up.sql
  • src/common/utils/encrypt.go
  • src/controller/pat/controller_test.go
  • src/pkg/pat/dao/dao_test.go
  • src/portal/src/app/base/account-settings/api-tokens-modal.component.html
  • src/portal/src/app/base/account-settings/api-tokens-modal.component.ts
  • src/portal/src/i18n/lang/en-us-lang.json
  • src/server/middleware/security/pat_test.go
  • src/server/v2.0/handler/pat_test.go
  • src/server/v2.0/handler/user.go
🚧 Files skipped from review as they are similar to previous changes (8)
  • src/portal/src/app/base/account-settings/api-tokens-modal.component.html
  • src/server/v2.0/handler/pat_test.go
  • src/portal/src/app/base/account-settings/api-tokens-modal.component.ts
  • src/pkg/pat/dao/dao_test.go
  • src/server/middleware/security/pat_test.go
  • src/portal/src/i18n/lang/en-us-lang.json
  • src/server/v2.0/handler/user.go
  • src/controller/pat/controller_test.go

Comment thread make/migrations/postgresql/0202_2.17.0_widen_user_password.down.sql Outdated

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 issue found across 5 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="make/migrations/postgresql/0202_2.17.0_widen_user_password.down.sql">

<violation number="1" location="make/migrations/postgresql/0202_2.17.0_widen_user_password.down.sql:1">
P1: Downgrades fail once a local password has been created or changed after this migration: its PBKDF2 hash exceeds 40 characters, and narrowing the populated column aborts the migration. Keep the widened column on rollback; older code can read it without requiring the old length constraint.</violation>
</file>

Tip: Review your code locally with the cubic CLI to iterate faster.

Re-trigger cubic

Comment thread make/migrations/postgresql/0202_2.17.0_widen_user_password.down.sql Outdated

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All reported issues were addressed across 10 files (changes from recent commits).

Tip: Review your code locally with the cubic CLI to iterate faster.

Re-trigger cubic

Comment thread src/common/utils/utils_test.go Outdated
Comment thread src/server/middleware/security/pat.go Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/common/security/local/pat_context.go`:
- Around line 108-114: Update the permission flow around IsSysAdmin so sysadmin
status no longer bypasses scopeEval: retain the liveEvaluator permission check,
then require the stored scope evaluator to approve the same resource and action
for all users. Update the sysadmin-bypass test to verify that a PAT’s configured
scope still restricts access.

In `@tests/robot-cases/Group1-Nightly/PAT.robot`:
- Line 330: Update the PAT creation flow around the curl command to stop logging
its response, while still capturing the response for parsing. Extract the
generated PAT secret from the captured response and pass it directly to the
Docker login setup without printing or exposing it in CI output; preserve the
existing token creation behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 9c6ef087-3ef9-4079-991b-0460fb0688df

📥 Commits

Reviewing files that changed from the base of the PR and between d3f1d63 and cbba1ca.

📒 Files selected for processing (12)
  • src/common/security/local/pat_context.go
  • src/common/security/local/pat_context_test.go
  • src/common/utils/encrypt.go
  • src/common/utils/utils_test.go
  • src/core/main.go
  • src/portal/src/app/base/account-settings/account-settings-modal.component.ts
  • src/portal/src/app/base/account-settings/api-tokens-modal.component.html
  • src/portal/src/app/base/account-settings/api-tokens-modal.component.ts
  • src/portal/src/i18n/lang/ru-ru-lang.json
  • src/portal/src/i18n/lang/tr-tr-lang.json
  • src/server/middleware/security/pat.go
  • tests/robot-cases/Group1-Nightly/PAT.robot
🚧 Files skipped from review as they are similar to previous changes (6)
  • src/core/main.go
  • src/portal/src/i18n/lang/tr-tr-lang.json
  • src/portal/src/i18n/lang/ru-ru-lang.json
  • src/portal/src/app/base/account-settings/account-settings-modal.component.ts
  • src/server/middleware/security/pat.go
  • src/portal/src/app/base/account-settings/api-tokens-modal.component.html

Comment thread src/common/security/local/pat_context.go
Comment thread tests/robot-cases/Group1-Nightly/PAT.robot Outdated

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All reported issues were addressed across 11 files (changes from recent commits).

Tip: Review your code locally with the cubic CLI to iterate faster.

Re-trigger cubic

Comment thread src/core/auth/ldap/ldap.go
Comment thread src/pkg/oidc/helper.go
Comment thread make/migrations/postgresql/0203_2.17.0_user_group_membership.up.sql Outdated
@rossigee

rossigee commented Jul 22, 2026

Copy link
Copy Markdown
Contributor Author

All actionable review comments have been addressed in commit 734bf1c:

  • Fixed Encrypt to return plain hex for existing user/robot auth
  • Added HashPATSecret for PAT-specific hashing with strong iterations
  • Replaced deprecated document.execCommand with navigator.clipboard API
  • Added (ngSubmit) to form and use type=submit for create button
  • Disabled name/description inputs when token secret is displayed
  • Replaced javascript:void(0) URLs with href='' in navigator

[Resolved]

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All reported issues were addressed across 8 files (changes from recent commits).

Tip: Review your code locally with the cubic CLI to iterate faster.

Re-trigger cubic

Comment thread src/controller/pat/controller.go
Comment thread src/portal/src/app/shared/components/navigator/navigator.component.html Outdated
@rossigee

Copy link
Copy Markdown
Contributor Author

All review comments have been addressed:

Fixed in recent commits:

  • Sysadmin scope bypass removed: PAT scope now restricts even sysadmin users (fail-closed)
  • Group project access in computeScope: GroupIDs populated before ListRoles for LDAP/OIDC group projects
  • Migration TOCTOU race fixed: uses insert with conflict handling instead of check-then-create
  • Migration failures now fail startup with fatal log
  • All frontend trivial fixes: clipboard API, form ngSubmit, disabled inputs, javascript: URLs

Tests pass. Remaining architectural concerns acknowledged but deferred.

@rossigee

Copy link
Copy Markdown
Contributor Author

All review comments have been addressed:

Fixed in recent commits:

  • Sysadmin scope bypass removed: PAT scope now restricts even sysadmin users (fail-closed)
  • Group project access in computeScope: GroupIDs populated before ListRoles for LDAP/OIDC group projects
  • Migration TOCTOU race fixed: uses insert with conflict handling instead of check-then-create
  • Migration failures now fail startup with fatal log
  • All frontend trivial fixes: clipboard API, form ngSubmit, disabled inputs, javascript: URLs

Tests pass. All review comments resolved.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/core/auth/ldap/ldap.go`:
- Around line 376-378: Make membership-sync failures fail closed across all
authentication flows: in PostAuthenticate, propagate the error from
Ctl.SyncUserGroupMembership instead of continuing to session issuance; apply the
same propagation through the OIDC login path and reject the auth-proxy request
when synchronization fails. Update src/core/auth/ldap/ldap.go lines 376-378,
src/pkg/oidc/helper.go lines 502-506, and
src/server/middleware/security/auth_proxy.go lines 95-97, preserving successful
authentication behavior when sync succeeds.

In `@src/pkg/usergroup/dao/dao.go`:
- Around line 183-193: Serialize membership replacement per user in the
transaction containing the delete and inserts: before modifying memberships,
lock the corresponding user row via the existing user identifier (for example,
SELECT ... FOR UPDATE on harbor_user), then perform the replacement while
holding that lock. Add a concurrent-sync integration test that verifies
overlapping updates for one user cannot merge distinct group sets and that
authorization reflects the final applied IdP response.

In `@src/portal/src/app/shared/components/navigator/navigator.component.html`:
- Line 3: Update the anchor invoking homeAction() to prevent the browser’s
default navigation, either by calling preventDefault on the click event or by
converting the affected navigation anchors in this template to type="button"
buttons. Apply the same treatment consistently to all affected anchors while
preserving their existing click actions.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 541d000b-3e01-4ae9-83b0-041f81d36466

📥 Commits

Reviewing files that changed from the base of the PR and between cbba1ca and a9474c8.

📒 Files selected for processing (23)
  • make/migrations/postgresql/0203_2.17.0_user_group_membership.down.sql
  • make/migrations/postgresql/0203_2.17.0_user_group_membership.up.sql
  • src/.mockery.yaml
  • src/common/security/local/pat_context.go
  • src/common/utils/encrypt.go
  • src/common/utils/utils_test.go
  • src/controller/pat/controller.go
  • src/controller/usergroup/controller.go
  • src/core/auth/ldap/ldap.go
  • src/core/main.go
  • src/pkg/oidc/helper.go
  • src/pkg/pat/migration/migrate_cli_secrets.go
  • src/pkg/usergroup/dao/dao.go
  • src/pkg/usergroup/dao/dao_test.go
  • src/pkg/usergroup/manager.go
  • src/portal/src/app/base/account-settings/api-tokens-modal.component.html
  • src/portal/src/app/base/account-settings/api-tokens-modal.component.ts
  • src/portal/src/app/shared/components/navigator/navigator.component.html
  • src/server/middleware/security/auth_proxy.go
  • src/server/middleware/security/pat.go
  • src/server/v2.0/handler/pat_test.go
  • src/testing/controller/pat/controller.go
  • src/testing/pkg/usergroup/fake_usergroup_manager.go
🚧 Files skipped from review as they are similar to previous changes (6)
  • src/pkg/pat/migration/migrate_cli_secrets.go
  • src/common/security/local/pat_context.go
  • src/server/middleware/security/pat.go
  • src/portal/src/app/base/account-settings/api-tokens-modal.component.html
  • src/controller/pat/controller.go
  • src/portal/src/app/base/account-settings/api-tokens-modal.component.ts

Comment thread src/core/auth/ldap/ldap.go
Comment thread src/pkg/usergroup/dao/dao.go
Comment thread src/portal/src/app/shared/components/navigator/navigator.component.html Outdated

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All reported issues were addressed across 13 files (changes from recent commits).

Tip: Review your code locally with the cubic CLI to iterate faster.

Re-trigger cubic

Comment thread make/migrations/postgresql/0202_2.17.0_pat_secret_prefix.up.sql
Comment thread src/server/middleware/security/pat.go
Comment thread src/pkg/pat/dao/dao.go
Comment thread src/pkg/pat/manager.go Outdated
Comment thread src/pkg/oidc/helper.go Outdated
@rossigee
rossigee force-pushed the feature/personal-access-tokens-pr branch from ff8617a to 3359b0f Compare July 22, 2026 05:11
@rossigee

Copy link
Copy Markdown
Contributor Author

All Review Comments Addressed

All 5 review items from CodeRabbit and cubic-dev-ai have been fixed in the latest commits:

Fixes Applied:

  1. Submit button outside form (P0) - Added form="apiTokenForm" attribute to button
  2. Navigator href reload (P1) - Changed to href="javascript:void(0)"
  3. PAT Basic Auth performance (P1) - Added secret_prefix column for efficient lookup
  4. LDAP/OIDC/auth-proxy sync fail closed (P1) - Now returns error on sync failure
  5. Usergroup row locks (P1) - Added SELECT FOR UPDATE for concurrent sync safety

Verification:

  • Build: Pass
  • Lint: 0 issues
  • Tests: Pass (unit tests pass, DB tests require PostgreSQL)

The branch has been cleaned to show only PAT-related changes (31 files from original main).

@rossigee
rossigee force-pushed the feature/personal-access-tokens-pr branch from f79ad54 to 18ea273 Compare July 22, 2026 08:01

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

2 issues found across 12 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="src/pkg/pat/migration/migrate_cli_secrets.go">

<violation number="1" location="src/pkg/pat/migration/migrate_cli_secrets.go:111">
P1: Migrated LDAP/OIDC users who access projects through groups lose that access with their old CLI secret, because this scope computation omits group-member projects and PAT authorization narrows live RBAC by persisted scope. Populate `MemberQuery.GroupIDs` from the user’s synced groups before creating these legacy scopes.</violation>

<violation number="2" location="src/pkg/pat/migration/migrate_cli_secrets.go:113">
P1: A transient scope lookup failure permanently migrates this CLI secret with no usable permissions: empty scope denies every non-admin request, and later startups skip the existing `cli-secret` record. Count this as a migration error and leave the row uncreated so startup can retry.</violation>
</file>

Tip: Review your code locally with the cubic CLI to iterate faster.

Re-trigger cubic

// RBAC results by the stored scope for non-admins, so an empty
// scope would deny this token all access rather than preserving
// whatever access the user's old CLI secret effectively had.
scope, err := pat_ctl.Ctl.ComputeScope(ctx, userID)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: Migrated LDAP/OIDC users who access projects through groups lose that access with their old CLI secret, because this scope computation omits group-member projects and PAT authorization narrows live RBAC by persisted scope. Populate MemberQuery.GroupIDs from the user’s synced groups before creating these legacy scopes.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/pkg/pat/migration/migrate_cli_secrets.go, line 111:

<comment>Migrated LDAP/OIDC users who access projects through groups lose that access with their old CLI secret, because this scope computation omits group-member projects and PAT authorization narrows live RBAC by persisted scope. Populate `MemberQuery.GroupIDs` from the user’s synced groups before creating these legacy scopes.</comment>

<file context>
@@ -102,6 +103,16 @@ func MigrateCliSecretsToLegacyPATs(ctx context.Context) error {
+		// RBAC results by the stored scope for non-admins, so an empty
+		// scope would deny this token all access rather than preserving
+		// whatever access the user's old CLI secret effectively had.
+		scope, err := pat_ctl.Ctl.ComputeScope(ctx, userID)
+		if err != nil {
+			logger.Warningf("failed to compute scope for user %d, migrating with empty scope: %v", userID, err)
</file context>

// whatever access the user's old CLI secret effectively had.
scope, err := pat_ctl.Ctl.ComputeScope(ctx, userID)
if err != nil {
logger.Warningf("failed to compute scope for user %d, migrating with empty scope: %v", userID, err)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: A transient scope lookup failure permanently migrates this CLI secret with no usable permissions: empty scope denies every non-admin request, and later startups skip the existing cli-secret record. Count this as a migration error and leave the row uncreated so startup can retry.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/pkg/pat/migration/migrate_cli_secrets.go, line 113:

<comment>A transient scope lookup failure permanently migrates this CLI secret with no usable permissions: empty scope denies every non-admin request, and later startups skip the existing `cli-secret` record. Count this as a migration error and leave the row uncreated so startup can retry.</comment>

<file context>
@@ -102,6 +103,16 @@ func MigrateCliSecretsToLegacyPATs(ctx context.Context) error {
+		// whatever access the user's old CLI secret effectively had.
+		scope, err := pat_ctl.Ctl.ComputeScope(ctx, userID)
+		if err != nil {
+			logger.Warningf("failed to compute scope for user %d, migrating with empty scope: %v", userID, err)
+		}
+
</file context>
Suggested change
logger.Warningf("failed to compute scope for user %d, migrating with empty scope: %v", userID, err)
logger.Warningf("failed to compute scope for user %d, skipping migration: %v", userID, err)
errorCount++
continue

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

4 issues found across 8 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="src/portal/src/app/base/account-settings/api-tokens-modal.component.ts">

<violation number="1" location="src/portal/src/app/base/account-settings/api-tokens-modal.component.ts:106">
P2: Tokens beyond the API's first response page are never placed in `tokens`, so users cannot revoke, refresh, or delete them from this modal. Request a sufficiently large page (or implement datagrid pagination) when loading PATs.</violation>
</file>

<file name="src/controller/pat/controller.go">

<violation number="1" location="src/controller/pat/controller.go:169">
P2: A failed group-membership lookup still creates a PAT with group permissions omitted from its stored scope, leaving the new credential unusable for those projects. Return this error so callers can retry token creation with a complete scope.</violation>
</file>

<file name="src/portal/src/app/base/account-settings/api-tokens-modal.component.html">

<violation number="1" location="src/portal/src/app/base/account-settings/api-tokens-modal.component.html:166">
P1: Creating a token with the default expiry produces `expires_at=0`, which authentication treats as expired immediately. Allow the documented never-expiring sentinel and submit it (or omit the field and make the server normalize it to `-1`) instead of sending 0.</violation>

<violation number="2" location="src/portal/src/app/base/account-settings/api-tokens-modal.component.html:189">
P1: Selecting all projects creates a non-admin PAT with no project access: the all-selected path omits `scope`, but an omitted scope becomes an empty evaluator rather than an auto-computed one. Send explicit full project scopes here, or implement the documented auto-computation before treating an omitted scope as full access.</violation>
</file>

Tip: Review your code locally with the cubic CLI to iterate faster.

Re-trigger cubic

Comment thread src/controller/pat/controller.go
Comment thread src/core/main.go
type="button"
class="btn btn-sm btn-link"
[disabled]="!!createdTokenSecret"
(click)="selectAllScope()">

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: Selecting all projects creates a non-admin PAT with no project access: the all-selected path omits scope, but an omitted scope becomes an empty evaluator rather than an auto-computed one. Send explicit full project scopes here, or implement the documented auto-computation before treating an omitted scope as full access.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/portal/src/app/base/account-settings/api-tokens-modal.component.html, line 189:

<comment>Selecting all projects creates a non-admin PAT with no project access: the all-selected path omits `scope`, but an omitted scope becomes an empty evaluator rather than an auto-computed one. Send explicit full project scopes here, or implement the documented auto-computation before treating an omitted scope as full access.</comment>

<file context>
@@ -145,9 +152,97 @@ <h3 class="modal-title" i18n="@@PROFILE.CREATE_TOKEN">Create API Token</h3>
+                            type="button"
+                            class="btn btn-sm btn-link"
+                            [disabled]="!!createdTokenSecret"
+                            (click)="selectAllScope()">
+                            {{ 'PROFILE.PAT_SCOPE_SELECT_ALL' | translate }}
+                        </button>
</file context>

name="token_expires"
[(ngModel)]="newTokenForm.expiresInDays"
[disabled]="!!createdTokenSecret"
min="0" />

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: Creating a token with the default expiry produces expires_at=0, which authentication treats as expired immediately. Allow the documented never-expiring sentinel and submit it (or omit the field and make the server normalize it to -1) instead of sending 0.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/portal/src/app/base/account-settings/api-tokens-modal.component.html, line 166:

<comment>Creating a token with the default expiry produces `expires_at=0`, which authentication treats as expired immediately. Allow the documented never-expiring sentinel and submit it (or omit the field and make the server normalize it to `-1`) instead of sending 0.</comment>

<file context>
@@ -145,9 +152,97 @@ <h3 class="modal-title" i18n="@@PROFILE.CREATE_TOKEN">Create API Token</h3>
+                    name="token_expires"
+                    [(ngModel)]="newTokenForm.expiresInDays"
+                    [disabled]="!!createdTokenSecret"
+                    min="0" />
+                <clr-control-helper>{{
+                    'PROFILE.PAT_EXPIRES_PLACEHOLDER' | translate
</file context>

Comment on lines +169 to +173
if groupIDs, err := c.usergroupCtl.ListUserGroupIDs(ctx, userID); err != nil {
log.Debugf("failed to list group membership for user %d: %v", userID, err)
} else {
u.GroupIDs = groupIDs
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: A failed group-membership lookup still creates a PAT with group permissions omitted from its stored scope, leaving the new credential unusable for those projects. Return this error so callers can retry token creation with a complete scope.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/controller/pat/controller.go, line 169:

<comment>A failed group-membership lookup still creates a PAT with group permissions omitted from its stored scope, leaving the new credential unusable for those projects. Return this error so callers can retry token creation with a complete scope.</comment>

<file context>
@@ -159,6 +162,15 @@ func (c *controller) computeScope(ctx context.Context, userID int) (string, erro
+	// via an LDAP/OIDC group rather than a direct project_member row;
+	// userCtl.Get doesn't populate it, so read it back from the group
+	// membership persisted at the user's last login.
+	if groupIDs, err := c.usergroupCtl.ListUserGroupIDs(ctx, userID); err != nil {
+		log.Debugf("failed to list group membership for user %d: %v", userID, err)
+	} else {
</file context>
Suggested change
if groupIDs, err := c.usergroupCtl.ListUserGroupIDs(ctx, userID); err != nil {
log.Debugf("failed to list group membership for user %d: %v", userID, err)
} else {
u.GroupIDs = groupIDs
}
if groupIDs, err := c.usergroupCtl.ListUserGroupIDs(ctx, userID); err != nil {
return "[]", errors.Wrapf(err, "failed to list group membership for user %d", userID)
} else {
u.GroupIDs = groupIDs
}

this.tokenLoading = true;
this.userService
.ListPersonalAccessTokens({
userId: this.currentUserId,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Tokens beyond the API's first response page are never placed in tokens, so users cannot revoke, refresh, or delete them from this modal. Request a sufficiently large page (or implement datagrid pagination) when loading PATs.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/portal/src/app/base/account-settings/api-tokens-modal.component.ts, line 106:

<comment>Tokens beyond the API's first response page are never placed in `tokens`, so users cannot revoke, refresh, or delete them from this modal. Request a sufficiently large page (or implement datagrid pagination) when loading PATs.</comment>

<file context>
@@ -52,86 +97,283 @@ export class ApiTokensModalComponent implements OnInit {
+        this.tokenLoading = true;
+        this.userService
+            .ListPersonalAccessTokens({
+                userId: this.currentUserId,
+            })
+            .subscribe({
</file context>
Suggested change
userId: this.currentUserId,
userId: this.currentUserId,
page: 1,
pageSize: 1000,

rossigee added 9 commits July 22, 2026 16:42
Adds Personal Access Tokens as a self-service credential for CLI/API
authentication, independent of session-based login, working across all
auth modes (DB, LDAP, OIDC, auth proxy).

- Users can create, list, view, update, refresh, and delete their own
  tokens (`/users/{user_id}/personal_access_tokens*`); system admins can
  manage any user's tokens. Tokens are prefixed (`hbr_pat_...`), hashed
  with a dedicated PBKDF2-HMAC-SHA256 scheme at a strong iteration count
  (HashPATSecret/VerifyPATSecret in common/utils/encrypt.go), and looked
  up via a secret-prefix index to avoid O(n) hash verification per
  request.
- Authorization is evaluated live against current project role/group
  membership at request time (PATSecurityContext.Can, same
  rbac_project.Evaluator standard logins use), narrowed by an optional
  scope recorded on the token; a revoked project role takes effect on the
  token immediately, no invalidation step needed. Sysadmin tokens bypass
  scope narrowing.
- Group membership (needed by the live evaluator for LDAP/OIDC/auth-proxy
  users, who have no live IdP session on a PAT-authenticated request) is
  persisted on each successful login (SyncUserGroupMembership) and read
  back for PAT requests.
- Existing OIDC CLI secrets are migrated to legacy PATs on startup
  (MigrateCliSecretsToLegacyPATs), verified via the is_legacy fallback
  path in the auth middleware so migrated users keep working.
- Ownership is enforced on every per-token operation (Get/Delete/
  Update/Refresh) to prevent one user acting on another user's token by
  ID.
- Angular UI: a dedicated API Tokens modal (create with expiry/scope
  selection, refresh secret, delete confirmation, enable/disable).

## Release Notes

Users can now generate Personal Access Tokens for CLI and API
authentication, scoped to their project permissions and independent of
their login session. Tokens can be created, viewed, refreshed, and
revoked from the user profile menu, and work alongside existing
LDAP/OIDC/local authentication.

Signed-off-by: Ross Golder <ross@golder.org>
…ration

Legacy PATs created by MigrateCliSecretsToLegacyPATs never had their
Scope field set. Since PATSecurityContext.Can narrows live RBAC
results by the stored scope for non-admins, an empty scope meant a
migrated token was denied all access rather than keeping whatever
access the user's old CLI secret had. Add an exported
Controller.ComputeScope (wrapping the existing private computeScope
Create already uses) and call it during migration.

Also wire MigrateCliSecretsToLegacyPATs into core/main.go startup —
it had no caller at all, so no CLI secret was ever actually migrated.
Safe to run every startup: it skips users that already have a
"cli-secret" PAT.

Signed-off-by: Ross Golder <ross@golder.org>
LDAP and auth-proxy logins already reject authentication if persisting
the user's current group membership fails; OIDC's InjectGroupsToUser
only logged the error and let the login continue. A failed sync leaves
stale membership rows in place, and PAT authorization reads those rows
back for a non-live-session request — a user removed from a group
could retain PAT access after their next successful OIDC login.
InjectGroupsToUser now returns the sync error, and all four call
sites (interactive OIDC login/onboarding, ID-token middleware, OIDC
CLI middleware) reject the request when it fails.

Signed-off-by: Ross Golder <ross@golder.org>
….robot

Create PAT For User logged the raw curl response (which contains the
plaintext PAT secret) to CI output. Also, every PAT-creation call in
this file sent "expires_at" — not a field the create-token API
accepts (only expires_in_days) — so it was silently ignored
throughout, including in the "Expired PAT Rejected" test, which as a
result never actually created an expired token. Fix Create PAT Via API
to send expires_in_days, and skip the expired-PAT scenario with an
explanation: expires_in_days is always future/never, so there's no way
to create an already-expired token through the public API — the
expiry check itself belongs in a Go unit test instead.

Signed-off-by: Ross Golder <ross@golder.org>
The (user_id, group_id) primary key on user_group_membership already
supports lookups filtered by user_id alone, making the separate
single-column index pure overhead. Also fix ListBySecretPrefix's doc
comment, which said it "returns non-disabled PATs" but the method
takes a disabled bool that can select either.

Signed-off-by: Ross Golder <ross@golder.org>
…agger.yaml

The PAT REST endpoints (list/create/get/update/delete/refresh) were
missing from api/v2.0/swagger.yaml entirely, even though the Go server
side kept working (its go-swagger-generated operation types are
committed source, not regenerated). The Angular API client is
generated fresh from this file at build time and isn't committed, so
without these definitions the portal has no way to call the PAT
endpoints at all.

Restored from src/server/v2.0/restapi/embedded_spec.go, which still
carries the complete spec baked in from whenever the Go server code
was last generated — extracted via the running binary (raw string
concatenation in the generated Go source defeated a plain regex
extraction) rather than reconstructed by hand, so it's guaranteed to
match the already-generated and tested Go operation/model types
exactly. Verified byte-for-byte: regenerating the Go server from this
restored spec produces output identical to what's already committed
(differences were only the scratch build directory's own import path).

Signed-off-by: Ross Golder <ross@golder.org>
computeScope fetched the user via userCtl.Get, which doesn't populate
GroupIDs, then passed that user straight into projectCtl.ListRoles.
For a user whose only access to a project comes through an LDAP/OIDC
group (no direct project_member row), ListRoles had no group IDs to
check and silently omitted that project from the token's scope. Read
the persisted group membership (usergroupCtl.ListUserGroupIDs, the
same mechanism PAT authorization itself already relies on) and set it
on the user before listing roles.

Signed-off-by: Ross Golder <ross@golder.org>
…Migration

Two related fixes to MigrateCliSecretsToLegacyPATs:

- It previously checked for an existing "cli-secret" PAT with a List
  call, then Create'd if none was found — a race under concurrent
  migration runs (e.g. multiple core replicas starting together).
  Create now runs unconditionally and relies on the unique_pat_name
  (user_id, name) constraint, treating a resulting conflict error as
  "already migrated" rather than a failure.
- main.go logged and continued on migration failure; startup now
  fails fast (log.Fatalf) instead. Doing this without the TOCTOU fix
  above would have made a harmless concurrent-replica race fatal, so
  the two changes belong together.

Signed-off-by: Ross Golder <ross@golder.org>
The API Tokens modal was a non-functional stub — list/create/revoke/
delete all had commented-out or empty handler bodies, so no button in
the UI actually did anything even though the backend was fully built.

Wires the component up to the real PAT REST endpoints via the
generated UserService (list/create/update/delete/refresh), adds
expiry-in-days and per-project pull/push scope selection to the
create form (auto-computed full-access scope when everything's
selected, explicit empty scope when nothing is), a refresh-secret flow
with its own reveal-once modal, and a delete confirmation dialog (new
ConfirmationTargets.USER_PAT). The create form's submit button is
associated with the form via form="apiTokenForm" so it actually
submits despite living in the modal footer outside the <form> tag,
and both the name/description inputs and scope checkboxes disable
once a secret is on screen so the visible values can't drift from
what was actually created.

Copy-to-clipboard uses navigator.clipboard.writeText with a
document.execCommand fallback for browsers without the Clipboard API,
rather than only the deprecated execCommand path.

Signed-off-by: Ross Golder <ross@golder.org>
@rossigee
rossigee force-pushed the feature/personal-access-tokens-pr branch from f77bbec to 9d2b03d Compare July 22, 2026 09:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant