Skip to content

feat(jira): add Cloud OAuth 2.0 client-credentials auth - #152

Merged
psturc merged 9 commits into
konflux-ci:mainfrom
mfrancisc:jiraoauthsupport
Sep 23, 2026
Merged

psturc merged 9 commits into
konflux-ci:mainfrom
mfrancisc:jiraoauthsupport

Conversation

@mfrancisc

Copy link
Copy Markdown

Summary

  • Add Jira Cloud OAuth 2.0 (service account / 2LO) as a connection auth method alongside email + API token.
  • Store clientId, clientSecret, and cloudId; mint a 60-minute Bearer token via grant_type=client_credentials against https://auth.atlassian.com/oauth/token.
  • Call Jira through https://api.atlassian.com/ex/jira/{cloudId}/rest/ and remint the access token near expiry or on 401. No user consent / refresh-token flow.
  • Config UI: Cloud auth radio for API Token vs OAuth 2.0 (Service Account). Existing Server Basic/PAT paths are unchanged.

Service-account 2LO can mint 60-minute Bearer tokens and call the
Atlassian API gateway, so Jira Cloud connections no longer require a
user API token.

Upstream-Status: Pending

Co-authored-by: Cursor <cursoragent@cursor.com>
@mfrancisc
mfrancisc requested a review from a team as a code owner September 7, 2026 14:35
@fullsend-ai-review

fullsend-ai-review Bot commented Sep 7, 2026 •

Copy link
Copy Markdown

🤖 Finished Review · ✅ Success · Started 2:36 PM UTC · Completed 2:59 PM UTC

Commit: 9ee3c25 · View workflow run →

Runtime: claude · Model: opus → claude-opus-4-6 · Effort: high · Cost: $7.97

@fullsend-ai-review fullsend-ai-review Bot added the risk/moderate PR risk: moderate label Sep 7, 2026
@fullsend-ai-review

fullsend-ai-review Bot commented Sep 7, 2026 •

Copy link
Copy Markdown

Risk Assessment: elevated (3/5)

Details

Tier 1 signals unchanged from prior assessment (Tier1=2.25); re-review anchoring preserves score 3 — large blast radius, 3 security-sensitive OAuth/token files, and ~1700 lines of new authentication code across dormant core connection files remain the primary risk drivers.

Previous run

Risk Assessment: elevated (3/5)

Details

Tier 1 signals unchanged from prior assessment (Tier1=2.25); re-review anchoring preserves score 3 — the large blast radius, 3 security-sensitive OAuth/token files, and near-2000 lines of new authentication code across dormant core connection files remain the primary risk drivers.

Previous run (2)

Risk Assessment: elevated (3/5)

Details

Tier 1 and Tier 2 signals unchanged from prior assessment (Tier1=2.25, Tier2=2.71); large blast radius, 3 security-sensitive OAuth/token files, and high historical fix rates on core connection and auth files that have been dormant 2-3 years.

Previous run (3)

Risk Assessment: elevated (3/5)

Details

This PR adds OAuth 2.0 client-credentials authentication to the Jira plugin, touching 3 security-sensitive files across a large blast radius (1629 lines, 18 files) while modifying core connection and API client files that have been dormant for 2-3 years, producing a composite elevated score of 3 (Tier1=62%x2.25 + Tier2=38%x2.75 = 2.54 rounded to 3).

Previous run (4)

Risk Assessment: elevated (3/5)

Details

Score 3 (elevated) driven by large blast radius (1629 lines across 18 files spanning backend Go and frontend TypeScript), 3 security-sensitive files introducing OAuth2 client-credential storage and Bearer-token minting, and only 22% test file ratio; moderated by an experienced author, stable low-churn codebase, no CI or dependency changes, and no protected-path touches.

Previous run (5)

Risk Assessment: moderate (2/5)

Details

Large-blast-radius OAuth2 feature (18 files, 1519 lines, 3 security-sensitive paths) is offset by strong test coverage, no churned or contested files in git history, no CI or dependency changes, and a known non-first-time author, yielding a moderate composite score of 2.

Previous run (6)

Risk Assessment: moderate (2/5)

Details

Moderate risk: PR introduces OAuth 2.0 client-credentials auth across 18 files/1363 lines with large blast radius and 3 security-sensitive touch points, but structural mitigators (no protected paths, no CI or dependency changes, stable low-churn file history, tests present) keep the composite score at 2, consistent with the prior assessment.

Previous run (7)

Risk Assessment: moderate (2/5)

Details

Moderate risk: the PR introduces OAuth 2.0 client-credentials auth across 1200 lines with a large blast radius and 3 security-sensitive touch points, but structural risk mitigators (no protected paths, no CI or dependency changes, stable file history) keep the composite score at 2.

@fullsend-ai-review

fullsend-ai-review Bot commented Sep 7, 2026 •

Copy link
Copy Markdown

Review

Findings

High

  • [missing-authorization] — This PR introduces a substantial new feature (OAuth 2.0 authentication for Jira Cloud, including DB migration, new token subpackage, and Config UI changes) with no linked issue. Non-trivial changes require explicit authorization via a linked issue or equivalent work item.
    Remediation: Link a JIRA or GitHub issue that authorizes the addition of OAuth 2.0 client-credentials support for Jira Cloud.

Low

  • [error-handling] config-ui/src/utils/request.ts:65 — The isPluginConnectionTest guard exempts all 401 responses on /plugins/.../test URLs from the login redirect. The backend already maps Jira-originated 401/403 to HTTP 400 (BadInput), making the frontend guard redundant for Jira. For other plugins that do not remap, the guard is useful. However, a genuine DevLake session-expiry 401 on a test endpoint would be suppressed.
    Remediation: Consider adding a code comment explaining the belt-and-suspenders design: backend 400-mapping for Jira + frontend guard for other plugins.

  • [documentation-gap] docs/upstream-diffs.md — The upstream-diffs.md OAuth 2.0 section lists changed files but omits config-ui/src/utils/request.ts, which modifies the global 401 interceptor — a cross-cutting change that benefits all plugins.
    Remediation: Add config-ui/src/utils/request.ts to the file list and note the cross-cutting 401-interceptor change.

  • [injection] config-ui/src/plugins/register/jira/connection-fields/auth.tsx:33 — The gatewayEndpoint function interpolates user-provided cloudId into a URL template without client-side validation. The backend's sanitizedCloudID regex overwrites before use, so there is no exploitable path, but client-side validation provides defense-in-depth.
    Remediation: Add a client-side validation check matching the backend's cloudIDPattern (/^[a-zA-Z0-9-]{1,64}$/).

  • [edge-case] backend/plugins/jira/token/token_provider.go:83 — needsRefresh returns false when expiresAt is nil but token is non-empty. The invariant that cacheToken always sets a non-nil expiresAt is implicit.
    Remediation: Treat nil expiresAt with a non-empty token as needing refresh, or add a comment documenting the invariant.

  • [data-hygiene] backend/plugins/jira/models/connection.go:194 — When MergeFromRequest detects an auth-method change, it does not zero the now-unused OAuth2 fields (ClientId, ClientSecret, CloudId). Stale credentials remain in the database.
    Remediation: Zero the stale OAuth2 fields when switching auth methods.

  • [unauthorized-change] backend/plugins/jira/api/connection_api.go:73 — testConnection now returns errors.BadInput (HTTP 400) instead of errors.HttpStatus(401) for credential failures for ALL Jira connection types, silently altering the API contract for existing users.
    Remediation: Document this API contract change in upstream-diffs.md. Consider whether the frontend guard makes the backend remapping unnecessary.

  • [doc-style] backend/plugins/jira/models/connection.go:84 — Four exported methods lack doc comments: IsOAuth2(), OAuthAccessToken(), OAuthAccessTokenExpiresAt(), SetOAuthAccessToken().
    Remediation: Add one-line doc comments per Go convention.

  • [doc-style] backend/plugins/jira/models/connection.go:174 — CustomValidate is an exported method implementing a pluginhelper interface but has no doc comment.
    Remediation: Add a doc comment explaining the interface delegation.


Next steps:

  • /fs-fix — agent addresses review findings automatically
  • /fs-fix <your instruction> — agent fixes with your specific guidance
  • Push commits directly — review re-runs automatically on push
  • /fs-fix-stop — disable automatic fix runs for this PR
Previous run

Review

Findings

Medium

  • [naming-convention] backend/plugins/jira/models/connection.go:38 — AUTH_METHOD_OAUTH2 uses SCREAMING_SNAKE_CASE, which is not idiomatic Go. Go convention is MixedCaps for exported identifiers, including constants. The rest of the auth subsystem uses CamelCase names (BasicAuth, AccessToken, MultiAuth). Referenced across multiple files and tests.
    Remediation: Rename to AuthMethodOAuth2 and update all callers.

  • [consumer-completeness] backend/plugins/jira/tasks/api_client.go:106 — The ignoreHTTPStatus404 callback returns errors.Unauthorized.New("authentication failed, please check your AccessToken") on HTTP 401. For OAuth2 connections where the RefreshRoundTripper retries once and then the persistent 401 reaches this callback, the message is misleading — OAuth2 users configured client credentials, not a PAT-style access token.
    Remediation: Update to a generic message (e.g., "authentication failed, please check your credentials") or branch on auth method.

  • [upstream-divergence-tracking-gap] docs/upstream-diffs.md — This PR adds the isPluginConnectionTest guard to config-ui/src/utils/request.ts, but the new jira-OAuth upstream-diffs entry does not list request.ts. The OIDC entry attributes this change, yet the code actually lands in this PR — the provenance is misleading.
    Remediation: Add config-ui/src/utils/request.ts to the jira-OAuth entry, or update the OIDC entry to note the guard was deferred to this PR.

Low

  • [injection] config-ui/src/plugins/register/jira/connection-fields/auth.tsx:33 — Frontend gatewayEndpoint interpolates cloudId into a URL template without validation. Backend mitigates via sanitizedCloudID regex (^[a-zA-Z0-9-]{1,64}$) and ApplyGatewayEndpoint re-derivation, but defense-in-depth suggests frontend validation.
    Remediation: Add frontend validation matching the backend cloudIDPattern.

  • [error-handling] backend/plugins/jira/models/oauth.go:79 — MintOAuthAccessToken creates context.WithTimeout(context.Background(), ...) instead of accepting a caller-provided context. Task cancellation is not propagated to the token-mint HTTP request (bounded to 10s timeout).
    Remediation: Accept a context.Context parameter and propagate from callers.

  • [edge-case] backend/plugins/jira/api/connection_api.go:44 — Validation changed from StructExcept to ValidateConnection, which is stricter for non-OAuth2 auth methods. A BasicAuth test-connection request with an empty Username that previously passed will now be rejected. This is arguably a correctness improvement.

  • [data-exposure] backend/plugins/jira/api/connection_api.go:238 — jiraHTTPErrorDetail reflects up to 500 chars of remote Jira error response verbatim to API consumers. Non-HTML, non-JSON text is forwarded without sanitization. Risk is limited to admin-visible connection-test error messages.

  • [naming-conventions] backend/plugins/jira/models/connection.go:43 — cloudIDPattern uses two-cap ID while struct field CloudId uses one-cap Id, creating within-file inconsistency.
    Remediation: Align to one convention (cloudIdPattern to match existing struct fields, or CloudID with updated tags).

  • [import-ordering] backend/plugins/jira/api/connection_api.go:35 — Third-party import mapstructure grouped with local imports instead of in a separate block. Pre-existing but not fixed by this PR.
    Remediation: Run goimports -local github.com/apache/incubator-devlake -w.

  • [scope-alignment] config-ui/src/types/connection.ts — Adding cloudId, clientId, clientSecret to shared IConnectionAPI and IConnection interfaces continues the trajectory of plugin-specific fields in shared types. Follows existing precedent (appId, secretKey, dbUrl).


Next steps:

  • /fs-fix — agent addresses review findings automatically
  • /fs-fix <your instruction> — agent fixes with your specific guidance
  • Push commits directly — review re-runs automatically on push
  • /fs-fix-stop — disable automatic fix runs for this PR
Previous run (2)

Review

Findings

Medium

  • [error handling / logic] config-ui/src/utils/request.ts:46 — The isPluginConnectionTest guard suppresses the login redirect for ALL 401 responses on /plugins/.../test URLs. When a DevLake session expires while the user is on the connection form and they click ‘Test Connection’, the auth middleware returns 401, the guard matches the URL, and the redirect to /login is suppressed. The user sees a confusing connection-test error instead of being sent to the login page. Note: the guard remains load-bearing for non-Jira plugins that may still forward raw 401 for bad credentials.
    Remediation: Distinguish session-expiry 401s from upstream-credential 401s via response body shape or a custom header rather than URL pattern matching.

  • [design-incoherence] backend/plugins/jira/tasks/api_client.go:36 — Two independent token caches exist and neither feeds the other. NewApiClientFromConnection invokes PrepareApiClient on JiraConn, which mints an OAuth2 token. Immediately afterward, a new TokenProvider is created with an empty cache. apiClient.SetAuthFunction(nil) disables SetupAuthentication, so the token from PrepareApiClient is never used. The TokenProvider mints a second token on its first GetToken() call.
    Remediation: Seed the TokenProvider with the already-minted token, or remove PrepareApiClient since the RefreshRoundTripper supersedes it for the task execution path.

  • [missing-authorization] backend/plugins/jira/models/oauth.go — This is a non-trivial feature addition (new auth method, DB migration, two new packages, shared frontend type changes) yet no linked JIRA issue is attached.
    Remediation: Link this PR to a JIRA issue that authorized the Jira Cloud OAuth 2.0 service-account auth method.

  • [missing-doc] backend/plugins/jira/README.md:17 — The Jira plugin README defers entirely to the upstream Apache DevLake website. This PR introduces a fork-specific OAuth 2.0 auth method with three new required configuration fields (clientId, clientSecret, cloudId). The upstream site will not document this fork-specific feature.
    Remediation: Add a section to the README or docs/ describing the OAuth 2.0 Service Account auth method, including required fields and endpoint behavior.

  • [breaking-api] backend/plugins/jira/api/connection_api.go:76 — The connection test endpoints previously returned HTTP 401 when remote Jira credentials were invalid. The PR replaces those with errors.BadInput (HTTP 400). Any API client branching on status == 401 to detect bad credentials will silently stop detecting the condition. The change is intentional and documented in code, and the Config UI is updated simultaneously.
    Remediation: Document the status-code change in a CHANGELOG or release notes.

  • [breaking-api] backend/plugins/jira/models/connection.go:36 — The Jira connection REST API introduces AUTH_METHOD_OAUTH2 = "OAuth2" as a new accepted authMethod value. Strict-schema consumers may reject the new value.
    Remediation: Update any OpenAPI/Swagger spec annotations that enumerate authMethod values to include "OAuth2".

Low

  • [edge case] backend/plugins/jira/api/connection_api.go:44 — Validation in testConnection changed from vld.StructExcept to connection.ValidateConnection. For non-OAuth2 connections, a test-connection request that omits BasicAuth credentials will now return a validation error instead of progressing to the HTTP call.

  • [secret-exposure] backend/plugins/jira/api/connection_api.go:243 — jiraHTTPErrorDetail reads up to 4096 bytes from the Jira upstream response body and returns up to 500 characters in error messages. It could disclose internal Jira server details. Mitigated by authenticated-only access.
    Remediation: Consider stripping or further limiting detail extracted from upstream response bodies.

  • [upstream-tracking] docs/upstream-diffs.md:62 — config-ui/src/utils/request.ts is listed only under the OIDC divergence section but the 401 interceptor change is equally motivated by the Jira OAuth2 feature. The jira OAuth section does not list this file.
    Remediation: Add a cross-reference note in the jira OAuth rebase notes.

  • [missing-ownership] backend/plugins/jira — No AGENTS.md for the jira plugin despite growing fork-specific modifications with multiple contributors.

  • [naming-convention] backend/plugins/jira/token/token_provider.go:31 — DefaultRefreshBuffer is exported but used only within the token package. All analogous constants in adjacent files are unexported.
    Remediation: Rename to defaultRefreshBuffer.

  • [naming-convention] backend/plugins/jira/models/connection.go:43 — cloudIDPattern and sanitizedCloudID use Go-idiomatic ID suffix, while the struct field CloudId uses the codebase-established Id convention.
    Remediation: Rename to cloudIdPattern and sanitizedCloudId for codebase consistency.

  • [naming-convention] backend/plugins/jira/tasks/api_client.go:43 — Error variable named terr instead of the conventional err (may be deliberate shadow avoidance).
    Remediation: Use var tp *token.TokenProvider; tp, err = ... to avoid both shadowing and non-conventional naming.

  • [breaking-api] backend/plugins/jira/models/connection.go:64 — Three new JSON-tagged fields added to JiraConn API responses. Additive and backward-compatible for lenient consumers.

  • [breaking-api] config-ui/src/types/connection.ts:35 — Shared IConnectionAPI and IConnection interfaces gain three new optional Jira-specific fields.

  • [breaking-api] config-ui/src/api/connection/index.ts:49 — Union type of accepted field names for test API functions gains Jira-specific fields.


Next steps:

  • /fs-fix — agent addresses review findings automatically
  • /fs-fix <your instruction> — agent fixes with your specific guidance
  • Push commits directly — review re-runs automatically on push
  • /fs-fix-stop — disable automatic fix runs for this PR
Previous run (3)

Review

Findings

High

  • [missing-authorization] backend/plugins/jira — This is a non-trivial feature PR (12 new or heavily modified Go files, 3 new UI files, a DB migration, a new token/ package) with no linked issue. Non-trivial changes require a linked issue to establish authorized scope and intent. The PR body describes the implementation well but there is no external authorization artifact (JIRA ticket, GitHub issue) to confirm this capability was planned and approved.
    Remediation: Link a JIRA/GitHub issue authorizing the Jira Cloud OAuth 2.0 service-account feature before merging.

Medium

  • [race condition] backend/plugins/jira/models/connection.go:71 — The oauthToken and oauthTokenExpiresAt fields on JiraConn have no structural synchronization. Concurrent safety relies on two implicit invariants: (1) TokenProvider.mu serializes reads/writes, and (2) SetAuthFunction(nil) (api_client.go:55) prevents SetupAuthentication from reading oauthToken concurrently. The SetAuthFunction(nil) invariant is documented inline but the implicit coupling means any future code path that re-installs an auth function would silently re-introduce the race.
    Remediation: Move oauthToken/oauthTokenExpiresAt into TokenProvider (structurally binding the mutex to the data), or add a sync.RWMutex to JiraConn.

Low

  • [architectural-coherence] backend/plugins/jira — The Jira plugin has no AGENTS.md file. The PR already correctly tracks divergences in upstream-diffs.md, but creating plugin-level ownership documentation would help as fork-specific code grows.
    Remediation: Create backend/plugins/jira/AGENTS.md documenting build/test commands, layout conventions, and OAuth2 design constraints.

  • [API contract violation] backend/plugins/jira/token/round_tripper.go:99 — ensureGetBody mutates the original *http.Request by setting GetBody, ContentLength, and replacing Body, which goes beyond the http.RoundTripper contract. The mutation is idempotent and the base transport receives a clone, so downstream transports are unaffected.
    Remediation: Clone the request before mutation, or document the deviation.

  • [edge case] config-ui/src/plugins/register/jira/connection-fields/auth.tsx:32 — gatewayEndpoint does not validate whitespace-only cloudId before constructing the URL. cloudId is truthy but cloudId.trim() yields '', producing a malformed URL transiently. Backend validation catches this before save.
    Remediation: Guard the template with cloudId?.trim() instead of cloudId.

  • [error handling] backend/plugins/jira/models/oauth.go:79 — MintOAuthAccessToken applies two independent 10-second timeouts (http.Client.Timeout and context.WithTimeout). The redundancy is benign but could produce confusing error messages under edge conditions.

  • [scope-alignment] config-ui/src/utils/request.ts:44 — The isPluginConnectionTest guard in the global 401 interceptor affects all plugins, not just Jira. The change is benign and correctly documented in upstream-diffs.md.

  • [architectural-coherence] backend/plugins/jira/models/connection.go:143 — ValidateConnection shadows the MultiAuth interface method to bypass the oneof validator constraint for OAuth2. If upstream changes MultiAuth.ValidateConnection, this shadow method becomes a silent divergence.
    Remediation: Add a rebase note to upstream-diffs.md that this shadow method must be audited on upstream MultiAuth changes.

  • [Naming conventions] backend/plugins/jira/tasks/api_client.go:43 — The error variable terr is unconventional; the codebase consistently reuses err in this pattern.
    Remediation: Replace terr with err.

  • [missing documentation for new feature] backend/plugins/jira/README.md:17 — The in-repo README has no indication that this fork supports OAuth 2.0 (Service Account / 2LO) auth for Jira Cloud.
    Remediation: Add a brief fork-specific note about the OAuth 2.0 auth method.


Next steps:

  • /fs-fix — agent addresses review findings automatically
  • /fs-fix <your instruction> — agent fixes with your specific guidance
  • Push commits directly — review re-runs automatically on push
  • /fs-fix-stop — disable automatic fix runs for this PR
Previous run (4)

Review

Findings

Medium

  • [missing-authorization] docs/upstream-diffs.md:293 — Feature-sized change (new auth mechanism, DB migration, new Go package, Config UI changes) with no linked JIRA issue. CLAUDE.md section 7.2 requires an Epic Link for Story/Task/Feature work.
    Remediation: Create or link a JIRA issue and reference it in the PR description before merge.

  • [API contract violation] backend/plugins/jira/token/round_tripper.go:99 — ensureGetBody mutates the original *http.Request passed to RoundTrip by setting req.GetBody, replacing req.Body, and updating req.ContentLength. The http.RoundTripper interface contract states: "RoundTrip should not modify the request, except for consuming and closing the Body." Currently safe because DevLake creates requests fresh per API call, but violates the Go stdlib contract.
    Remediation: Move the body snapshot into cloneRequestWithBearer so it operates on the clone rather than the original request.

Low

  • [Race condition (latent)] backend/plugins/jira/models/connection.go:71 — oauthToken and oauthTokenExpiresAt fields on JiraConn are unsynchronized. Thread safety is ensured by architectural convention (NewJiraApiClient clears authFunc after installing RefreshRoundTripper), documented in a code comment in api_client.go, but there is no compile-time or runtime guard.
    Remediation: Add a sync.RWMutex for the token fields, or document on SetupAuthentication that OAuth2 callers must not use it concurrently.

  • [design-divergence] backend/plugins/jira/models/connection.go:145 — ValidateConnection on JiraConn shadows MultiAuth.ValidateConnection to sidestep core's oneof constraint. Rationale is in code comments but not a formal ADR.
    Remediation: Add a brief ADR under docs/adr/ explaining the choice to keep OAuth2 plugin-local.

  • [fail-open] config-ui/src/utils/request.ts:65 — isPluginConnectionTest guard suppresses 401-to-login redirect for all plugin connection test URLs. The backend already remaps Jira 401/403 → 400, making this redundant for Jira. Other plugins may rely on the guard.
    Remediation: Document the guard's purpose or narrow it to only suppress for remote credential failures.

  • [design-coherence] backend/plugins/jira/models/connection.go:69 — OAuthTokenURL is a test-only override field in the production JiraConn struct (tagged json:"-" gorm:"-"). Tags prevent serialization impact but couple test infrastructure to the production model.
    Remediation: Pass the token URL as a parameter to MintOAuthAccessToken instead.

  • [Validation fragility] backend/plugins/jira/api/connection_api.go:44 — When connection.IsOAuth2() is true and vld is nil (test-time only), ValidateConnection is called with nil *validator.Validate. Works because the OAuth2 path doesn't use it, but fragile against future changes.

  • [Behavior change] backend/plugins/jira/api/connection_api.go:44 — testConnection validation changed from vld.StructExcept to connection.ValidateConnection, now validating the selected auth method's required fields. This is an improvement but a behavior change for the /plugins/jira/test endpoint.

  • [documentation comment format] backend/plugins/jira/token/token_provider.go:57 — Exported method GetToken() lacks a doc comment while NewTokenProvider() and ForceRefresh() are documented.
    Remediation: Add // GetToken returns a valid OAuth 2.0 access token, minting a new one if the cached token is absent or near expiry.

  • [documentation comment format] backend/plugins/jira/token/round_tripper.go:38 — Exported constructor NewRefreshRoundTripper() lacks a doc comment while the RefreshRoundTripper type and RoundTrip() method are documented.
    Remediation: Add // NewRefreshRoundTripper wraps base with a round tripper that automatically remints the OAuth 2.0 token on 401 responses.


Next steps:

  • /fs-fix — agent addresses review findings automatically
  • /fs-fix <your instruction> — agent fixes with your specific guidance
  • Push commits directly — review re-runs automatically on push
  • /fs-fix-stop — disable automatic fix runs for this PR
Previous run (5)

Review

Findings

Medium

  • [error-handling-gap] backend/plugins/jira/token/round_tripper.go:79 — cloneRequestWithBearer restores the request body for retry only when req.GetBody is non-nil. Go's http.NewRequest sets GetBody for *strings.Reader, *bytes.Reader, and *bytes.Buffer, but not for arbitrary io.Reader implementations. If a POST/PUT request uses a non-standard body type, the 401 retry will silently send an empty/consumed body.
    Remediation: Add a nil-body guard — when GetBody is nil and req.Body is non-nil, buffer the body upfront so GetBody can be synthesized, or skip the retry with a warning.

Low

  • [race-condition] backend/plugins/jira/models/connection.go:131 — SetupAuthentication reads jc.oauthToken without synchronization. Safe in practice because NewJiraApiClient clears the auth function via SetAuthFunction(nil), and testConnection is single-threaded. The invariant is documented at api_client.go:52-54.
  • [scope-creep] backend/plugins/jira/api/connection_api.go:80 — The 401/403 → errors.BadInput mapping applies to all auth methods, not only OAuth2. Well-motivated but extends beyond the stated scope.
  • [code-organization] backend/plugins/jira/api/connection_api.go:44 — The if/else-if block has identical bodies in both branches. Collapse to if connection.IsOAuth2() || vld != nil.
    Remediation: Collapse to a single condition.
  • [code-organization] backend/plugins/jira/api/connection_api.go:53 — Redundant ApplyGatewayEndpoint() call; ValidateConnection already calls it internally.
    Remediation: Remove the redundant call.
  • [dead-code] backend/plugins/jira/models/connection.go:154 — if jc.Endpoint == "" is unreachable. Preceding guards validate CloudId and ApplyGatewayEndpoint always produces a non-empty endpoint. The error message "cloudId is required" is also misleading post-validation.
    Remediation: Remove the dead guard or replace the error message with one describing the unexpected internal state.
  • [naming-conventions] backend/plugins/jira/models/connection.go:137 — fmt.Sprintf("Bearer %s", token) vs "Bearer "+token in round_tripper.go. Inconsistent within the same PR.
    Remediation: Normalise to "Bearer "+token.
  • [test-adequacy] backend/plugins/jira/token/token_provider_test.go:110 — No test covers concurrent ForceRefresh calls (the 401-retry thundering-herd scenario).
  • [logic] backend/plugins/jira/models/connection.go:157 — Type assertion connection.(*JiraConnection) always fails in the testConnection flow (receives *JiraConn), silently skipping name validation. Works by coincidence.
  • [API-contract-change] backend/plugins/jira/api/connection_api.go:83 — Jira HTTP 401/403 now mapped to 400. External API consumers not covered by the frontend fix.
  • [fail-open] config-ui/src/plugins/register/jira/connection-fields/auth.tsx:32 — Frontend gatewayEndpoint() lacks client-side cloudId validation. Server-side validation catches invalid values; cosmetic only.

Next steps:

  • /fs-fix — agent addresses review findings automatically
  • /fs-fix <your instruction> — agent fixes with your specific guidance
  • Push commits directly — review re-runs automatically on push
  • /fs-fix-stop — disable automatic fix runs for this PR
Previous run (6)

Review

Findings

Medium

  • [race-condition] backend/plugins/jira/models/connection.go:120 — Data race on JiraConn.oauthToken and oauthTokenExpiresAt. SetupAuthentication (line 120) reads jc.oauthToken via OAuthAccessToken() without synchronization. Concurrently, TokenProvider.refreshToken (invoked from RefreshRoundTripper on a different goroutine of the async client's worker pool) writes to jc.oauthToken via SetOAuthAccessToken. The TokenProvider.mu mutex serializes calls to GetToken/ForceRefresh, but does not protect direct field reads in SetupAuthentication. With the ApiAsyncClient using multiple workers, this is a data race per Go's memory model.
    Remediation: Either (a) skip setting the Authorization header in SetupAuthentication when IsOAuth2() is true and a RefreshRoundTripper is active, or (b) protect oauthToken/oauthTokenExpiresAt with a sync.RWMutex on JiraConn.

  • [injection] backend/plugins/jira/models/connection.go:89 — CloudId is interpolated directly into the API gateway URL via fmt.Sprintf without format validation. GatewayEndpoint() only trims whitespace but does not verify that CloudId is a valid Atlassian Cloud ID (UUID). A CloudId containing path traversal sequences would cause the backend to make requests to unintended URL paths. Impact is limited since the host is hardcoded, but unvalidated input in URL construction is a defense-in-depth concern.
    Remediation: Add a validation check in ValidateConnection that verifies CloudId matches the expected format using a regex like ^[a-f0-9-]{36}$ or at minimum rejects /, .., %, ?, #, and @.

Low

  • [edge-case] backend/plugins/jira/token/round_tripper.go:53 — RefreshRoundTripper retry sends consumed body for non-GET requests. http.Request.Clone makes a shallow copy of the Body field, so after base.RoundTrip(reqClone) reads the body, the retry clones req again inheriting the consumed body. Practical impact is negligible as Jira data collection is predominantly GET.
    Remediation: Save a body-restoration function via req.GetBody (if available) and use it to rebuild the body for the retry request.

  • [error-handling] backend/plugins/jira/api/connection_api.go:44 — When connection.IsOAuth2() is true and vld is nil, ValidateConnection is called with a nil *validator.Validate. This works today because the OAuth2 branch never uses the validator parameter, but creates a fragile coupling.
    Remediation: Guard the call: if connection.IsOAuth2() { ... } else if vld != nil { ... }.

  • [data-exposure] backend/plugins/jira/models/oauth.go:102 — When MintOAuthAccessToken receives a non-200 response, the response body (up to 512 characters) is included verbatim in the error message that propagates to the API caller.
    Remediation: Extract only structured error fields (e.g., JSON error and error_description per RFC 6749 section 5.2) rather than echoing the raw body.

  • [data-exposure] backend/plugins/jira/api/connection_api.go:246 — jiraHTTPErrorDetail reads the entire Jira response body via io.ReadAll without a size limit. Similarly, MintOAuthAccessToken (oauth.go:92) reads the full token endpoint response without a size limit.
    Remediation: Use io.LimitReader to cap the read size: io.ReadAll(io.LimitReader(res.Body, 4096)).

  • [scope-creep-shared-type] config-ui/src/types/connection.ts — cloudId, clientId, and clientSecret are added to the shared IConnectionAPI and IConnection interfaces. These are Jira-specific OAuth 2.0 fields. Follows the existing pattern (e.g., appId, secretKey, dbUrl) but further widens the shared type.

  • [abstraction-placement] backend/plugins/jira/models/oauth.go — The in-memory OAuth token state and token-minting logic are co-located on JiraConn in the models package. The separate token/ package wraps JiraConn to add thread-safety via sync.Mutex. This two-layer split means mutable token state is reachable from both layers without coordination — the root cause of the race condition above. See also: [race-condition] finding at connection.go:120.


Next steps:

  • /fs-fix — agent addresses review findings automatically
  • /fs-fix <your instruction> — agent fixes with your specific guidance
  • Push commits directly — review re-runs automatically on push
  • /fs-fix-stop — disable automatic fix runs for this PR
Previous run (7)

Review

Findings

Medium

  • [race condition] backend/plugins/jira/models/connection.go:120 — Data race on JiraConn.oauthToken between SetupAuthentication and MintOAuthAccessToken. SetupAuthentication reads jc.oauthToken without synchronization while TokenProvider.refreshToken() writes it under TokenProvider.mu. The RefreshRoundTripper subsequently overwrites the Authorization header making the stale read harmless at runtime, but this is a genuine data race that will trigger Go's race detector (-race flag).
    Remediation: Either skip calling SetupAuthentication for OAuth2 connections when the RefreshRoundTripper is active (e.g., clear the authFunc after installing the round tripper), or protect oauthToken/oauthTokenExpiresAt reads with a sync.RWMutex.

  • [injection] backend/plugins/jira/models/connection.go:89 — CloudId is interpolated into the API gateway URL via fmt.Sprintf without format validation beyond whitespace trimming. URL-special characters (../, ?, #) can manipulate the constructed URL, directing the bearer token to unintended Atlassian API paths. The frontend regex is trivially bypassed via direct API calls; the backend only checks that CloudId is non-empty.
    Remediation: Validate CloudId server-side to match the expected Atlassian UUID format (e.g., regexp.MustCompile("^[a-zA-Z0-9-]+$")).

Low

  • [request body consumption on retry] backend/plugins/jira/token/round_tripper.go:53 — req.Clone() shares the underlying Body io.Reader. If a 401 triggers a retry, the second clone has an already-consumed body, silently corrupting POST/PUT/PATCH retries. Currently only GET requests are used, matching the established GitHub round tripper pattern.
    Remediation: Save body bytes before first attempt (or use req.GetBody), or document the GET-only constraint.

  • [validation behavior change] backend/plugins/jira/api/connection_api.go:42 — Validation logic changed from vld.StructExcept (excluding both auth structs) to connection.ValidateConnection (validating the selected method). This is more correct but could surface new validation errors for existing non-OAuth2 connections.

  • [missing test coverage] backend/plugins/jira/token/round_tripper.go:61 — TestRoundTripper401Refresh only covers the success path. No tests for persistent 401 after refresh, ForceRefresh error, or GetToken error on retry.

  • [architectural-coherence] backend/plugins/jira/models/connection.go:130 — ValidateConnection intentionally shadows MultiAuth's oneof constraint for OAuth2. The bypass is documented in code comments and tested, but the dependency on core internals is untracked.
    Remediation: Add a comment or assertion that breaks visibly if the core MultiAuth constraint changes.

  • [architectural-coherence] backend/plugins/jira/models/connection.go:66 — In-memory OAuth token cache lives on the GORM model struct. Each task loads its own JiraConn, so tokens are re-minted per task rather than shared.
    Remediation: Consider isolating the token cache in TokenProvider to separate configuration from runtime session state.

  • [architectural-coherence] config-ui/src/types/connection.ts:35 — cloudId, clientId, clientSecret added to shared IConnectionAPI/IConnection interfaces, continuing the existing pattern of plugin-specific field accumulation.

  • [Documentation comment] backend/plugins/jira/token/round_tripper.go:43 — Exported RoundTrip method lacks a doc comment. The GitHub counterpart documents it per CLAUDE.md §18.
    Remediation: Add a doc comment consistent with github/token/round_tripper.go.

  • [Code organization] backend/plugins/jira/token/round_tripper.go:47 — roundTripWithRetry omits inline section comments present in the structurally identical GitHub implementation.
    Remediation: Mirror section comments from github/token/round_tripper.go.


Next steps:

  • /fs-fix — agent addresses review findings automatically
  • /fs-fix <your instruction> — agent fixes with your specific guidance
  • Push commits directly — review re-runs automatically on push
  • /fs-fix-stop — disable automatic fix runs for this PR

fullsend-ai-review[bot]

This comment was marked as outdated.

Credential 401s were forwarded as HTTP 401, so Config UI treated them as
session expiry. Map them to 400, skip the login interceptor for plugin
tests, and include Jira's response body (e.g. scope does not match).

Upstream-Status: Pending

Co-Authored-By: Cursor Grok 4.6 <noreply@example.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
@fullsend-ai-review

fullsend-ai-review Bot commented Sep 9, 2026 •

Copy link
Copy Markdown

🤖 Finished Review · ✅ Success · Started 11:21 AM UTC · Completed 11:46 AM UTC

Commit: 9ee3c25 · View workflow run →

Runtime: claude · Model: opus → claude-opus-4-6 · Effort: high · Cost: $9.34

fullsend-ai-review[bot]

This comment was marked as outdated.

Clear authFunc when the refresh round tripper is installed so collection
workers do not race on oauthToken. Validate CloudId, restore request
bodies on 401 retry, and stop echoing raw token-endpoint bodies.

Upstream-Status: Pending

Co-Authored-By: Cursor Grok 4.6 <noreply@example.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
@fullsend-ai-review

fullsend-ai-review Bot commented Sep 9, 2026 •

Copy link
Copy Markdown

🤖 Finished Review · ✅ Success · Started 5:52 PM UTC · Completed 6:18 PM UTC

Commit: e515ef1 · View workflow run →

Runtime: claude · Model: opus → claude-opus-4-6 · Effort: high · Cost: $9.81

fullsend-ai-review[bot]

This comment was marked as outdated.

mfrancisc and others added 2 commits September 10, 2026 12:12
Snapshot the original body when GetBody is missing so a reminted token
retry does not send an empty POST. Also drop the redundant gateway and
endpoint checks from Test Connection validation.

Upstream-Status: Pending

Co-Authored-By: Cursor Grok 4.6 <noreply@example.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
A 4KiB cap truncated Atlassian token JSON when many scopes were granted,
so Test Connection failed with unexpected end of JSON input. Keep a
64KiB DoS guard and fail clearly on empty or oversized bodies.

Upstream-Status: Pending

Co-Authored-By: Cursor Grok 4.6 <noreply@example.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
@fullsend-ai-review

fullsend-ai-review Bot commented Sep 10, 2026 •

Copy link
Copy Markdown

🤖 Finished Review · ✅ Success · Started 11:33 AM UTC · Completed 12:01 PM UTC

Commit: e515ef1 · View workflow run →

Runtime: claude · Model: opus → claude-opus-4-6 · Effort: high · Cost: $9.79

@fullsend-ai-review fullsend-ai-review Bot added risk/elevated PR risk: elevated and removed risk/moderate PR risk: moderate labels Sep 10, 2026
fullsend-ai-review[bot]

This comment was marked as outdated.

Rename AtlassianOAuthTokenURL so gosec does not treat the public
auth.atlassian.com endpoint as a hardcoded credential.

Upstream-Status: Pending

Co-Authored-By: Cursor Grok 4.6 <noreply@example.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
@fullsend-ai-review

fullsend-ai-review Bot commented Sep 10, 2026 •

Copy link
Copy Markdown

🤖 Finished Review · ✅ Success · Started 12:38 PM UTC · Completed 1:07 PM UTC

Commit: e515ef1 · View workflow run →

Runtime: claude · Model: opus → claude-opus-4-6 · Effort: high · Cost: $9.53

// can re-send it. http.NewRequest already sets GetBody for *bytes.Reader and
// *strings.Reader; collectors that pass an arbitrary io.Reader would otherwise
// retry with an empty body.
func ensureGetBody(req *http.Request) error {

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 — agree this should move into cloneRequestWithBearer to stay clean with the RoundTripper contract.

if req.GetBody != nil || req.Body == nil || req.Body == http.NoBody {
return nil
}
buf, err := io.ReadAll(req.Body)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Minor: io.ReadAll(req.Body) could use an io.LimitReader cap as a safety guard.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

it should be done now in e493d16 . Thanks!

fullsend-ai-review[bot]

This comment was marked as outdated.

Collection token state now lives under TokenProvider's mutex, and 401
retries snapshot the body onto clones so RoundTrip does not mutate the
original request.

Upstream-Status: Pending

Co-Authored-By: Cursor <cursoragent@cursor.com>
@fullsend-ai-review

fullsend-ai-review Bot commented Sep 11, 2026 •

Copy link
Copy Markdown

🤖 Finished Review · ✅ Success · Started 9:05 AM UTC · Completed 9:35 AM UTC

Commit: d9133a6 · View workflow run →

Runtime: claude · Model: opus → claude-opus-4-6 · Effort: high · Cost: $11.02

fullsend-ai-review[bot]

This comment was marked as outdated.

Collection builds the HTTP client without FromConnection so the round
tripper is the only minter and Authorization setter.

Upstream-Status: Pending

Co-Authored-By: Cursor <cursoragent@cursor.com>
@fullsend-ai-review

fullsend-ai-review Bot commented Sep 11, 2026 •

Copy link
Copy Markdown

🤖 Finished Review · ✅ Success · Started 11:52 AM UTC · Completed 12:16 PM UTC

Commit: d9133a6 · View workflow run →

Runtime: claude · Model: opus → claude-opus-4-6 · Effort: high · Cost: $8.02

fullsend-ai-review[bot]

This comment was marked as outdated.

fullsend-ai-review[bot]

This comment was marked as outdated.

@rsoaresd rsoaresd 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.

Amazing 🚀 I tested locally and it works pretty well!

@fullsend-ai-review

fullsend-ai-review Bot commented Sep 21, 2026 •

Copy link
Copy Markdown

🤖 Review · ❌ Terminated · Started 9:34 AM UTC · Ended 10:00 AM UTC

Commit: efeae75 · View workflow run →

@codecov-commenter

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 41.60%. Comparing base (c3c2f94) to head (271ce91).

❗ There is a different number of reports uploaded between BASE (c3c2f94) and HEAD (271ce91). Click for more details.

HEAD has 10 uploads less than BASE
Flag BASE (c3c2f94) HEAD (271ce91)
unit-tests-go 6 1
unit-tests-python 6 1
Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##             main     #152      +/-   ##
==========================================
- Coverage   48.05%   41.60%   -6.45%     
==========================================
  Files         154      154              
  Lines       10501    10501              
==========================================
- Hits         5046     4369     -677     
- Misses       5237     6019     +782     
+ Partials      218      113     -105     
Flag Coverage Δ
e2e-go 9.51% <ø> (-12.75%) ⬇️
unit-tests-go 35.89% <ø> (ø)
unit-tests-python 55.49% <ø> (ø)

Flags with carried forward coverage won't be shown. Click here to find out more.
see 27 files with indirect coverage changes


Continue to review full report in Codecov by Harness.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update c3c2f94...271ce91. Read the comment docs.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@fullsend-ai-review fullsend-ai-review 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.

Note: The following review comments could not be posted on the diff (GitHub returned 422) and are included here instead:

  • backend/plugins/jira/models/connection.go (file-level): Line 194 · [low] edge-case

When MergeFromRequest detects an auth-method change, it does not zero the now-unused OAuth2 fields (ClientId, ClientSecret, CloudId). Stale credentials remain in the database.

Suggested fix: Zero the stale OAuth2 fields when switching auth methods.

@fullsend-ai-review fullsend-ai-review 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.

See the review comment for full details.

status === 401 &&
!isLoginRoute() &&
!redirectingToLogin &&
!isPluginConnectionTest(requestUrl)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[low] error-handling

The isPluginConnectionTest guard exempts all 401 responses on /plugins/.../test URLs from the login redirect. The backend already maps Jira-originated 401/403 to HTTP 400, making the frontend guard redundant for Jira. A genuine DevLake session-expiry 401 on a test endpoint would be suppressed.

Suggested fix: Consider adding a code comment explaining the belt-and-suspenders design: backend 400-mapping for Jira + frontend guard for other plugins.

type CloudMethod = 'BasicAuth' | 'OAuth2';

const gatewayEndpoint = (cloudId?: string) =>
cloudId ? `https://api.atlassian.com/ex/jira/${cloudId.trim()}/rest/` : '';

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[low] injection

The gatewayEndpoint function interpolates user-provided cloudId into a URL template without client-side validation. The backend validates and overwrites, but client-side validation provides defense-in-depth.

Suggested fix: Add a client-side validation check matching the backend cloudIDPattern (/^[a-zA-Z0-9-]{1,64}$/).

if tp.token == "" {
return true
}
if tp.expiresAt == nil {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[low] edge-case

needsRefresh returns false when expiresAt is nil but token is non-empty. The invariant that cacheToken always sets a non-nil expiresAt is implicit.

Suggested fix: Treat nil expiresAt with a non-empty token as needing refresh, or add a comment documenting the invariant.

@@ -71,10 +71,12 @@ func testConnection(ctx context.Context, connection models.JiraConn) (*JiraTestC
return nil, errors.NotFound.New(fmt.Sprintf("Seems like an invalid Endpoint URL, please try %s", restUrl.String()))
}
if res.StatusCode == http.StatusUnauthorized || res.StatusCode == http.StatusForbidden {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[low] unauthorized-change

testConnection now returns errors.BadInput (HTTP 400) instead of errors.HttpStatus(401) for credential failures for ALL Jira connection types, silently altering the API contract for existing users.

Suggested fix: Document this API contract change in upstream-diffs.md. Consider whether the frontend guard makes the backend remapping unnecessary.

return *jc
}

func (jc *JiraConn) IsOAuth2() bool {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[low] doc-style

Four exported methods lack doc comments: IsOAuth2(), OAuthAccessToken(), OAuthAccessTokenExpiresAt(), SetOAuthAccessToken().

Suggested fix: Add one-line doc comments per Go convention.

return "_tool_jira_connections"
}

func (connection *JiraConnection) CustomValidate(entity interface{}, v *validator.Validate) errors.Error {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[low] doc-style

CustomValidate is an exported method implementing a pluginhelper interface but has no doc comment.

Suggested fix: Add a doc comment explaining the interface delegation.

@fullsend-ai-review

Copy link
Copy Markdown

🤖 Finished Review · ✅ Success · Started 9:34 AM UTC · Completed 10:00 AM UTC

Commit: efeae75 · View workflow run →

Runtime: claude · Model: opus → claude-opus-4-6 · Effort: high · Cost: $9.67

@mfrancisc

Copy link
Copy Markdown
Author

@kpiwko @flacatus @psturc I think it's safe to merge this one , as it was already merged upstream as well apache#9159

@psturc

psturc commented Sep 23, 2026

Copy link
Copy Markdown
Member

/ok-to-test

@psturc
psturc merged commit 978976c into konflux-ci:main Sep 23, 2026
26 of 27 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

risk/elevated PR risk: elevated

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants