Skip to content

fix(auth): align gated features with current license state#2000

Merged
yottahmd merged 5 commits intomainfrom
fix/license-feature
Apr 12, 2026
Merged

fix(auth): align gated features with current license state#2000
yottahmd merged 5 commits intomainfrom
fix/license-feature

Conversation

@yottahmd
Copy link
Copy Markdown
Collaborator

@yottahmd yottahmd commented Apr 12, 2026

Summary

  • make runtime license enforcement reflect the current active license state instead of relying on raw feature claims alone
  • keep expired licenses loaded locally so grace-period and expiry behavior can be enforced consistently across the backend and UI
  • make Docker-backed integration tests skip cleanly when the Docker daemon is unavailable so the OSS test suite stays reliable across environments

Why

Licensed paths were not enforced consistently after expiry. Some UI affordances only checked feature claims, OIDC gating was still tied to startup-time state, and heartbeat expiry handling kept stale data without making current entitlement availability explicit. That left long-running servers and the frontend out of sync with actual license validity.

Testing

  • go test ./... -count=1
  • pnpm -C ui exec tsc --noEmit

Summary by CodeRabbit

Release Notes

  • New Features

    • Added configurable grace period support for licenses based on license claims
    • License grace period now dynamically calculated per license instead of using fixed duration
  • Bug Fixes

    • Expired licenses now properly handled during grace period instead of clearing license state
    • Audit and SSO features now respect grace period status during license expiration
  • Tests

    • Added Docker daemon availability checks to integration tests for improved reliability
  • Chores

    • Updated license-related UI terminology to support broader license types beyond Pro
    • Improved license feature gating logic for audit and SSO capabilities

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 12, 2026

Important

Review skipped

Auto incremental reviews are disabled on this repository.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: a4164709-fb58-483e-85be-7c02cdab9a7a

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

This PR adds grace period support for license expiration, allowing licenses to remain functional for a configurable duration after expiry. It introduces Docker test environment helpers, updates license feature gating throughout the API/auth/OIDC handlers, modifies license state evaluation logic, and aligns UI terminology to reflect both license and trial variations.

Changes

Cohort / File(s) Summary
Docker Test Helpers
internal/intg/docker_helper_test.go, internal/intg/ct_test.go, internal/intg/mltcmd_test.go, internal/intg/redis_test.go, internal/intg/s3_test.go, internal/intg/sftp_test.go, internal/intg/ssh_test.go
Added requireDockerDaemon() and requireDockerClient() helper functions to gate integration tests on Docker availability; refactored existing Docker client initialization across multiple test files to use the new helpers instead of inline client.New() calls.
License Grace Period Support
internal/license/claims.go, internal/license/claims_test.go, internal/license/state.go, internal/license/state_test.go
Added GraceDays field to LicenseClaims struct for JSON serialization. Implemented graceDuration() method in State to compute per-license grace window; updated isInGracePeriod() to use claim-driven grace duration instead of fixed constant.
License Error Handling & Testing
internal/license/manager.go, internal/license/manager_test.go
Added HTTP 400 status code handling in heartbeat error switch to treat expired licenses as non-fatal with warning log; added test case verifying grace-period mode is retained on 400 response.
API Audit Feature Gating
internal/service/frontend/api/v1/api.go, internal/service/frontend/api/v1/auth.go
Added isAuditLicensed() helper to check both auditService presence and license.FeatureAudit flag; updated audit logging conditions in logAudit and Login to use the helper instead of direct nil checks.
OIDC/SSO Feature Gating
internal/service/frontend/auth/oidc.go
Added LicenseChecker field to BuiltinOIDCConfig; implemented license feature gate in both login and callback handlers to redirect when FeatureSSO is disabled with error message "SSO requires an active Dagu license".
Server Initialization & Template Updates
internal/service/frontend/server.go, internal/service/frontend/templates.go
Removed hard disabling of audit/OIDC services during NewServer; replaced with runtime license checks. Updated template conditionals for oidcEnabled and licenseValid to reflect grace period and feature flag state; changed terminal handler initialization to pass license checker.
Terminal Handler Licensing
internal/service/frontend/terminal/handler.go
Extended Handler struct with licenseChecker field; updated NewHandler signature to accept license checker; modified ServeHTTP to conditionally gate audit service based on FeatureAudit license flag.
UI License/Trial Terminology
ui/src/App.tsx, ui/src/components/LicenseBanner.tsx, ui/src/hooks/useLicense.ts, ui/src/pages/license/index.tsx, ui/src/pages/users/index.tsx
Unified UI copy to reference "Dagu license or trial" instead of hardcoded "Dagu Pro license"; added licenseLabel and renewalLabel based on plan type; updated useHasFeature() to require feature and valid/grace-period license state; reordered grace-period priority in license status display.

Sequence Diagram(s)

sequenceDiagram
    participant API as API Handler
    participant LM as License Manager
    participant State as License State
    participant Svc as Feature Service<br/>(Audit/SSO/OIDC)
    
    API->>LM: isFeatureEnabled(Feature)?
    LM->>State: IsFeatureEnabled(Feature)?
    State->>State: Check: expired?
    alt License Not Expired
        State-->>LM: true (feature enabled)
    else License Expired
        State->>State: Check: within graceDuration()?
        alt Within Grace Period
            State-->>LM: true (feature enabled)
        else Grace Period Expired
            State-->>LM: false (feature disabled)
        end
    end
    
    LM-->>API: feature allowed?
    
    alt Feature Allowed
        API->>Svc: Proceed with feature
        Svc-->>API: Success
    else Feature Denied
        API-->>API: Gate/Skip feature<br/>(null service or early return)
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

  • fix: replace app live polling with event-driven SSE updates #1968: Modifies internal/service/frontend/server.go Server struct initialization and wiring (this PR adjusts audit/license/terminal handler dependencies; related PR adds eventService/SSE integration), creating overlapping initialization concerns that may need coordinated merging.
🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 40.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the primary change: aligning gated features with the current license state through runtime enforcement rather than startup-time snapshots.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/license-feature

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 and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🧹 Nitpick comments (2)
internal/intg/ct_test.go (1)

503-507: Consider removing duplicate daemon probes where requireDockerClient(t) is already used.

In these blocks, requireDockerDaemon(t) is immediately followed by requireDockerClient(t), which re-checks daemon availability. Collapsing to a single requireDockerClient(t) call would reduce redundant probe calls.

Also applies to: 1088-1093, 1287-1292, 1400-1405

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/intg/ct_test.go` around lines 503 - 507, Remove redundant
requireDockerDaemon(t) calls when they are immediately followed by
requireDockerClient(t); keep only requireDockerClient(t) (which already probes
the daemon) and remove the duplicate requireDockerDaemon(t) before it in the
same setup blocks (e.g., the block that calls test.Setup(t),
requireDockerDaemon(t), then requireDockerClient(t)). Apply the same change to
other similar blocks in this file where requireDockerClient(t) follows
requireDockerDaemon(t) so only requireDockerClient(t) remains.
internal/intg/docker_helper_test.go (1)

14-29: Optional: extract shared daemon-probe logic to a small internal helper.

Both functions duplicate creation/probe logic; consolidating it would reduce maintenance drift if probe behavior changes later.

Also applies to: 31-49

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/intg/docker_helper_test.go` around lines 14 - 29, The
requireDockerDaemon function duplicates docker creation/probe logic—extract that
into a small internal helper (e.g., newDockerClientOrSkip or probeDockerDaemon)
that encapsulates client.New(client.FromEnv), context creation/timeout, Info
probe and t.Skip behavior; replace requireDockerDaemon and the other duplicate
block (lines 31-49) to call the new helper, returning the client, cancel/cleanup
function, and/or context so callers can defer Close()/cancel; ensure helper uses
the same t *testing.T to call t.Skipf with the original error messages.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@internal/license/state.go`:
- Around line 109-119: The issue is that graceDuration() reads
s.claims.GraceDays which is currently shallow-copied by cloneClaims, allowing
external mutation to affect entitlement checks; update cloneClaims to deep-copy
the GraceDays pointer (allocate a new int64/int/whatever the type is and copy
the value) so the internal State.claims holds its own independent pointer, and
ensure Claims()/Update() continue to use cloneClaims so
IsFeatureEnabled/IsGracePeriod and graceDuration() observe an immutable copy.

In `@internal/service/frontend/server.go`:
- Around line 1078-1082: The agent audit path is using the raw auditSvc passed
into initAgentAPI(...) before WithLicenseManager sets up srv.licenseManager,
allowing agent tool executions to bypass license-based audit gating; fix by
making initAgentAPI receive the live license.Checker (from
srv.licenseManager.Checker()) or by moving the initAgentAPI(...) call to after
WithLicenseManager is applied (i.e., after srv.licenseManager is wired), and
ensure any agent audit hooks created inside initAgentAPI use that
license.Checker instead of the standalone auditSvc; update references to
initAgentAPI, auditSvc, srv.licenseManager, license.Checker, and any agent audit
hook registration so they use the live checker.

In `@ui/src/components/LicenseBanner.tsx`:
- Around line 64-72: The banner currently uses a hardcoded/graceEnd value that
can be incorrect; update LicenseBanner (and its callers) to accept the
backend-provided grace end (e.g., a prop named graceEnd or graceEndDate) and
render that value instead of the hardcoded 14-day date, and if the prop is not
provided render a non-specific message without a concrete date (e.g., omit
{graceEnd} and say "soon" or remove the date fragment) so the UI is accurate for
trials/non-default grace periods; locate references to LicenseBanner, the
variables licenseLabel, graceEnd, renewalLabel and LICENSE_CONSOLE_URL to apply
the prop addition and add a safe fallback when undefined.

In `@ui/src/pages/license/index.tsx`:
- Around line 119-129: The deactivate control is only shown when license.valid
is true, so when a file/env-backed license has expired into license.gracePeriod
the deactivate card becomes hidden; update the visibility check used for that
card (where it currently checks license.valid) to allow both active and grace
states (e.g., use license.valid || license.gracePeriod) so the deactivate
section remains available during grace period, and verify any related rendering
branches that treat gracePeriod distinctly still render the correct status
badges (license.gracePeriod, license.valid, license.community).

---

Nitpick comments:
In `@internal/intg/ct_test.go`:
- Around line 503-507: Remove redundant requireDockerDaemon(t) calls when they
are immediately followed by requireDockerClient(t); keep only
requireDockerClient(t) (which already probes the daemon) and remove the
duplicate requireDockerDaemon(t) before it in the same setup blocks (e.g., the
block that calls test.Setup(t), requireDockerDaemon(t), then
requireDockerClient(t)). Apply the same change to other similar blocks in this
file where requireDockerClient(t) follows requireDockerDaemon(t) so only
requireDockerClient(t) remains.

In `@internal/intg/docker_helper_test.go`:
- Around line 14-29: The requireDockerDaemon function duplicates docker
creation/probe logic—extract that into a small internal helper (e.g.,
newDockerClientOrSkip or probeDockerDaemon) that encapsulates
client.New(client.FromEnv), context creation/timeout, Info probe and t.Skip
behavior; replace requireDockerDaemon and the other duplicate block (lines
31-49) to call the new helper, returning the client, cancel/cleanup function,
and/or context so callers can defer Close()/cancel; ensure helper uses the same
t *testing.T to call t.Skipf with the original error messages.
🪄 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: 4b7a7194-d04b-41ef-bedd-e9dde9003ba6

📥 Commits

Reviewing files that changed from the base of the PR and between 64b39d6 and bbd4c50.

📒 Files selected for processing (24)
  • internal/intg/ct_test.go
  • internal/intg/docker_helper_test.go
  • internal/intg/mltcmd_test.go
  • internal/intg/redis_test.go
  • internal/intg/s3_test.go
  • internal/intg/sftp_test.go
  • internal/intg/ssh_test.go
  • internal/license/claims.go
  • internal/license/claims_test.go
  • internal/license/manager.go
  • internal/license/manager_test.go
  • internal/license/state.go
  • internal/license/state_test.go
  • internal/service/frontend/api/v1/api.go
  • internal/service/frontend/api/v1/auth.go
  • internal/service/frontend/auth/oidc.go
  • internal/service/frontend/server.go
  • internal/service/frontend/templates.go
  • internal/service/frontend/terminal/handler.go
  • ui/src/App.tsx
  • ui/src/components/LicenseBanner.tsx
  • ui/src/hooks/useLicense.ts
  • ui/src/pages/license/index.tsx
  • ui/src/pages/users/index.tsx

@yottahmd yottahmd merged commit 080cbf0 into main Apr 12, 2026
6 checks passed
@yottahmd yottahmd deleted the fix/license-feature branch April 12, 2026 14:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant