Skip to content

feat(jans-cedarling): updating policies from URL by ttl#14174

Open
haileyesus2433 wants to merge 50 commits into
mainfrom
jans-cedarling-14127
Open

feat(jans-cedarling): updating policies from URL by ttl#14174
haileyesus2433 wants to merge 50 commits into
mainfrom
jans-cedarling-14127

Conversation

@haileyesus2433

@haileyesus2433 haileyesus2433 commented May 29, 2026

Copy link
Copy Markdown
Contributor

Prepare


Description

Target issue

closes #issue-number-here

Implementation Details


Test and Document the changes

  • Static code analysis has been run locally and issues have been fixed
  • Relevant unit and integration tests have been added/updated
  • Relevant documentation has been updated if any (i.e. user guides, installation and configuration guides, technical design docs etc)

Please check the below before submitting your PR. The PR will not be merged if there are no commits that start with docs: to indicate documentation changes or if the below checklist is not selected.

  • I confirm that there is no impact on the docs due to the code changes in this PR.

Closes #14127,

Summary by CodeRabbit

  • New Features

    • Opt-in background refresh for remote policy stores with configurable interval, atomic in-memory swaps, three-stage fetch strategy, byte-hash short-circuiting, conditional/HEAD-aware fetching, capped backoff with jitter, and updated authz snapshot behavior.
    • Cache-validator-aware HTTP fetch APIs and persisted sparse operational telemetry for refresh outcomes, strategies, streaks and cumulative counters.
  • Documentation

    • Updated docs and README examples describing refresh configuration, semantics, cache-header rules, and emitted metrics.

… validation

- Added a new `policy_store_refresh` module to handle background refresh of remote policy stores.
- Introduced `CacheValidators` to parse and manage HTTP cache headers for conditional GET requests.
- Updated `MetricsCollector` to track policy store refresh attempts and outcomes.
- Updated `PolicyStoreConfig` to include a refresh interval setting for URL-based sources.
- Implemented logic to handle `304 Not Modified` responses efficiently, ensuring the system remains responsive to policy updates.

Signed-off-by: haileyesus2433 <haileyesusbe@gmail.com>
- Introduced a new `policy_store_refresh` module to manage background refresh of remote policy stores.
- Updated `Cedarling` struct to utilize `ArcSwap` for atomic updates of the `Authz` instance.
- Added support for a configurable refresh interval for policy stores in `BootstrapConfig`.
- Implemented logic to spawn a refresh worker based on the configured refresh interval.
- Enhanced deserialization for policy store refresh interval to ensure proper clamping of values.

Signed-off-by: haileyesus2433 <haileyesusbe@gmail.com>
- Updated multiple benchmark files to include default values for `policy_store_config` in `BootstrapConfig`.
- Ensured consistency across `authz_authorize_multi_issuer_benchmark.rs`, `authz_authorize_unsigned_benchmark.rs`, `context_data_store_benchmark.rs`, and `startup_benchmark.rs`.

Signed-off-by: haileyesus2433 <haileyesusbe@gmail.com>
…ons in examples

- Added default values for `policy_store_config` in multiple example files including `authorize_unsigned.rs`, `bulk_authorization_benchmark.rs`, `lock_integration.rs`, `log_init.rs`, `profiling_multi_issuer.rs`, and `profiling_unsigned.rs`.
- Ensured consistency across examples to streamline configuration setup.

Signed-off-by: haileyesus2433 <haileyesusbe@gmail.com>
…ons in test files

- Updated `policy_store_config` in `policy_store_loader.rs`, `ssa_validation_integration.rs`, and `cedarling_util.rs` to include default values.
- Ensured consistency across test files to streamline configuration setup.

Signed-off-by: haileyesus2433 <haileyesusbe@gmail.com>
- Updated authorization methods in `blocking.rs` to utilize the `load()` method for improved instance management.
- Refactored the refresh worker spawning logic in `lib.rs` into a new `maybe_spawn_refresh_worker` function for better clarity and maintainability.
- Improved cache header handling in `cache_headers.rs` by simplifying the `from_headers` method and ensuring proper handling of cache control directives.
- Enhanced `RefreshState` struct in `policy_store_refresh.rs` to streamline initialization and improve clarity in managing refresh states.

Signed-off-by: haileyesus2433 <haileyesusbe@gmail.com>
@haileyesus2433 haileyesus2433 added this to the 2.2.0 milestone May 29, 2026
@haileyesus2433 haileyesus2433 self-assigned this May 29, 2026
@coderabbitai

coderabbitai Bot commented May 29, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

Adds opt-in background refresh for URL policy stores: new refresh-interval config, cache-header parsing and conditional GET/HEAD, loader metadata seeding and body-hash short-circuit, a refresh-worker strategy ladder that parses/rebuilds and atomically swaps Authz via ArcSwap, refresh telemetry, and docs/examples updates.

Changes

Policy Store Refresh Feature

Layer / File(s) Summary
Configuration & refresh interval propagation
src/bootstrap_config/raw_config/config.rs, src/bootstrap_config/policy_store_config.rs, src/bootstrap_config/decode.rs, src/bootstrap_config/mod.rs, examples/*, benches/*, tests/*
Add policy_store_refresh_interval_secs to raw bootstrap config; add refresh_interval_secs and refresh_enabled() on PolicyStoreConfig; decode now propagates the interval; update defaults and many struct literals to use ..Default::default().
HTTP cache header parsing and conditional fetch
src/http/cache_headers.rs, src/http/mod.rs, http_utils/src/lib.rs
New CacheHeadersState parses ETag, Last-Modified, Cache-Control and Expires. HttpClient adds get_bytes_conditional() and head_validators(); test-only GET helpers are gated; HttpRequestError gains classifiers and an HttpStatusError variant.
Policy store loader metadata & URL parsing
src/init/policy_store.rs
load_policy_store() now returns LoadedPolicyStore { store, body_hash, validators }; URL/LockServer loaders seed validators and body_hash; add lock-master and .cjar byte-parsing helpers; loader tests updated.
Background refresh worker with strategy ladder
src/init/policy_store_refresh.rs, src/init/mod.rs
New refresh module implements worker loop and strategy ladder (ConditionalHeadThenGetPlainGet), scheduling (server hint, backoff, jitter, min clamp), byte-hash short-circuit, parse/rebuild/swap via AuthzRebuilder and ArcSwap, error classification, shutdown handle, and comprehensive unit tests.
Cedarling Authz ArcSwap wiring
src/lib.rs, src/blocking.rs, src/init/service_factory.rs, src/init/service_config.rs
Cedarling stores Authz in ArcSwap and may spawn the refresh worker; Cedarling::new performs bootstrap load and may spawn refresh worker; public sync/async auth and policy-matching paths snapshot via .load(); ServiceFactory exposes http_client_for_refresh(); ServiceConfig constructor removed.
Metrics collection and refresh observability
src/authz/metrics.rs
MetricsCollector adds atomics for refresh timestamps, consecutive failures, last outcome/strategy, transition counters, and per-outcome cumulative counters; new recording methods and sparse emission into operational_stats are implemented and tested.
Docs, examples, bindings & tests
docs/*, bindings/*/README.md, examples/*, benches/*, src/tests/*
Document CEDARLING_POLICY_STORE_REFRESH_INTERVAL, describe refresh behavior and operational_stats keys; update language-binding READMEs and many example/benchmark/test struct literals to include ..Default::default().

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related issues

  • #14245 — Adds similar policy-store-refresh atomics grouping; related to telemetry layout and metrics introduced here.

Possibly related PRs

Suggested reviewers

  • dagregi
  • tareknaser
  • olehbozhok
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately describes the main change: adding periodic policy store refresh from URLs controlled by TTL/interval.
Description check ✅ Passed The description follows the template with all required sections completed including target issue, implementation details confirmed, and tests/docs verification.
Linked Issues check ✅ Passed The PR comprehensively implements all major objectives from issue #14127: background refresh worker, HTTP cache header support, conditional requests, body hash short-circuiting, exponential backoff, metrics, and error handling.
Out of Scope Changes check ✅ Passed All changes are directly aligned with refresh implementation scope: HTTP cache headers, conditional fetch APIs, policy store refresh worker, metrics, and related configuration. No unrelated changes detected.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

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

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch jans-cedarling-14127

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.

@mo-auto

mo-auto commented May 29, 2026

Copy link
Copy Markdown
Member

Snyk checks have passed. No issues have been found so far.

Status Scan Engine Critical High Medium Low Total (0)
Open Source Security 0 0 0 0 0 issues

💻 Catch issues earlier using the plugins for VS Code, JetBrains IDEs, Visual Studio, and Eclipse.

@mo-auto mo-auto added comp-jans-cedarling Touching folder /jans-cedarling kind-feature Issue or PR is a new feature request labels May 29, 2026
@mo-auto

mo-auto commented May 29, 2026

Copy link
Copy Markdown
Member

Error: Hi @haileyesus2433, You did not reference an open issue in your PR. I attempted to create an issue for you.
Please update that issues' title and body and make sure I correctly referenced it in the above PRs body.

haileyesus2433 and others added 4 commits June 1, 2026 03:31
store

Introduce a multi-tiered refresh strategy (Conditional → HeadThenGet →
PlainGet) that automatically degrades when the upstream server fails to
support efficient cache validation. Includes a periodic probe mechanism
to
attempt upgrades back to the efficient path and uses AHasher for faster
body hashing.

Signed-off-by: haileyesus2433 <haileyesusbe@gmail.com>
Track detailed refresh outcomes and strategy transitions in the metrics
collector. Counters are emitted sparsely—omitting zero values—to allow
dashboards to distinguish between missing metrics and genuine zero
readings.

Signed-off-by: haileyesus2433 <haileyesusbe@gmail.com>
validators

Added `head_validators` to `HttpClient` to probe upstream servers for
caching headers before a full GET request. This supports a fallback
strategy where clients can detect if a server supports HEAD-based
validation or requires a full fetch. Exposes `HeadOutcome` to handle
cases where HEAD is explicitly rejected (405/501).

Signed-off-by: haileyesus2433 <haileyesusbe@gmail.com>
@haileyesus2433 haileyesus2433 marked this pull request as ready for review June 2, 2026 06:06
@haileyesus2433

Copy link
Copy Markdown
Contributor Author

@coderabbitai please review

@coderabbitai

coderabbitai Bot commented Jun 2, 2026

Copy link
Copy Markdown
Contributor

@haileyesus2433 I'll review the PR right away!

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🤖 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 `@jans-cedarling/cedarling/src/bootstrap_config/policy_store_config.rs`:
- Around line 28-31: The constant PolicyStoreConfig::MIN_REFRESH_INTERVAL_SECS
is declared pub but not re-exported from the module root, so change its
visibility to the more restrictive pub(crate) unless you intend it to be part of
the public API; if it should be public instead, add it to the module exports
(mod.rs / lib root) alongside PolicyStoreConfig, PolicyStoreConfigRaw, and
PolicyStoreSource so it is truly exported. Locate MIN_REFRESH_INTERVAL_SECS in
the PolicyStoreConfig impl and either make it pub(crate) or add an explicit
re-export for it in the module root to match the crate's export policy.

In `@jans-cedarling/cedarling/src/bootstrap_config/raw_config/config.rs`:
- Around line 447-454: Add unit tests that exercise BootstrapConfigRaw's
deserialize_policy_store_refresh_interval path by decoding env vars for
CEDARLING_POLICY_STORE_REFRESH_INTERVAL and asserting
BootstrapConfigRaw.policy_store_refresh_interval_secs behaves like the other
refresh fields: when env var is unset the value is 0; when set to "0" it remains
0; when set to "1".."4" it is clamped to 5; when set to "5" or larger it is
unchanged. Use the same test harness used for other refresh-interval tests
(invoke the serde/env decoding path), reference BootstrapConfigRaw and
deserialize_policy_store_refresh_interval in the test names, and ensure each
case sets/clears the CEDARLING_POLICY_STORE_REFRESH_INTERVAL env var so tests
are isolated.

In `@jans-cedarling/cedarling/src/bootstrap_config/raw_config/json_util.rs`:
- Around line 92-107: Add a doc comment for the pub(super) deserializer function
deserialize_policy_store_refresh_interval explaining what it does: that it
accepts a u64 (or numeric string) via deserialize_or_parse_string_as_json,
treats 0 as "disabled" and returns 0, and otherwise clamps non-zero values up to
PolicyStoreConfig::MIN_REFRESH_INTERVAL_SECS; include expected input form,
behavior, and mention that it returns a normalized refresh interval or
deserialization error.

In `@jans-cedarling/cedarling/src/http/mod.rs`:
- Around line 261-267: The error construction in head_validators currently uses
HttpRequestReasonError::MaxRetriesExceeded and .with_retry_count(1), which is
misleading for a single-attempt HEAD probe; update the error to use a more
appropriate reason (e.g., HttpRequestReasonError::NetworkError or
HttpRequestReasonError::RequestFailed) and either remove the
.with_retry_count(1) call or set it to 0, and update the last_error message to
clarify this was a single probe failure; adjust the HttpRequestError::new
invocation accordingly so head_validators returns a clear, accurate error when
the HEAD send().await fails.
🪄 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: ASSERTIVE

Plan: Pro

Run ID: 9f4010f6-5509-40ec-8089-cbcc08dc1675

📥 Commits

Reviewing files that changed from the base of the PR and between e53ddad and f68bbde.

📒 Files selected for processing (27)
  • jans-cedarling/cedarling/benches/authz_authorize_multi_issuer_benchmark.rs
  • jans-cedarling/cedarling/benches/authz_authorize_unsigned_benchmark.rs
  • jans-cedarling/cedarling/benches/context_data_store_benchmark.rs
  • jans-cedarling/cedarling/benches/startup_benchmark.rs
  • jans-cedarling/cedarling/examples/authorize_unsigned.rs
  • jans-cedarling/cedarling/examples/bulk_authorization_benchmark.rs
  • jans-cedarling/cedarling/examples/lock_integration.rs
  • jans-cedarling/cedarling/examples/log_init.rs
  • jans-cedarling/cedarling/examples/profiling_multi_issuer.rs
  • jans-cedarling/cedarling/examples/profiling_unsigned.rs
  • jans-cedarling/cedarling/src/authz/metrics.rs
  • jans-cedarling/cedarling/src/blocking.rs
  • jans-cedarling/cedarling/src/bootstrap_config/decode.rs
  • jans-cedarling/cedarling/src/bootstrap_config/mod.rs
  • jans-cedarling/cedarling/src/bootstrap_config/policy_store_config.rs
  • jans-cedarling/cedarling/src/bootstrap_config/raw_config/config.rs
  • jans-cedarling/cedarling/src/bootstrap_config/raw_config/json_util.rs
  • jans-cedarling/cedarling/src/http/cache_headers.rs
  • jans-cedarling/cedarling/src/http/mod.rs
  • jans-cedarling/cedarling/src/init/mod.rs
  • jans-cedarling/cedarling/src/init/policy_store.rs
  • jans-cedarling/cedarling/src/init/policy_store_refresh.rs
  • jans-cedarling/cedarling/src/init/service_factory.rs
  • jans-cedarling/cedarling/src/lib.rs
  • jans-cedarling/cedarling/src/tests/policy_store_loader.rs
  • jans-cedarling/cedarling/src/tests/ssa_validation_integration.rs
  • jans-cedarling/cedarling/src/tests/utils/cedarling_util.rs

Comment thread jans-cedarling/cedarling/src/bootstrap_config/policy_store_config.rs Outdated
Comment thread jans-cedarling/cedarling/src/bootstrap_config/raw_config/config.rs
Comment thread jans-cedarling/cedarling/src/bootstrap_config/raw_config/json_util.rs Outdated
Comment thread jans-cedarling/cedarling/src/http/mod.rs
Signed-off-by: haileyesus2433 <haileyesusbe@gmail.com>
Document the new configuration property for enabling periodic policy
store refreshing via background workers for URL-based sources across
all language bindings and documentation.

Signed-off-by: haileyesus2433 <haileyesusbe@gmail.com>
@mo-auto mo-auto added area-documentation Documentation needs to change as part of issue or PR comp-docs Touching folder /docs labels Jun 2, 2026
/// Inserts `value` into `map` under `key` only when `value` is non-zero.
/// Used to keep "no observation yet" indistinguishable from missing in the
/// emitted metric map.
fn insert_if_nonzero(map: &mut HashMap<String, i64>, key: &str, value: i64) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Instead of i64, we can take atomic for usability, but outcome will be the same

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

resolved in ad28ae0

dagregi
dagregi previously approved these changes Jun 9, 2026
- Update `no-store` to zero out the in-memory freshness window
- Adopt refreshed cache headers from `304 Not Modified` responses
- Handle non-success HTTP status codes during HEAD probes explicitly
- Introduce `is_decode_error` and `is_http_status_error` to
  `HttpRequestError`

Signed-off-by: haileyesus2433 <haileyesusbe@gmail.com>
test assertions

- Update cache headers on 304 Not Modified responses.
- Introduce `RefreshWorkerSeed` to manage initial worker state.
- Add descriptive error messages to strategy and refresh tests.
- Fix typo in `RefreshStrategy` enum variant.

Signed-off-by: haileyesus2433 <haileyesusbe@gmail.com>
@haileyesus2433

Copy link
Copy Markdown
Contributor Author

Pls look closely at #14174 (review) (comments outside the diff). And create issues for all relevant points. It would be great to write links to the issues in the comments of this PR

most of the comments could be addresses in this PR so i have fixed them in c6c21ed i only left the cjar detection related comments which are being worked on in #14201 by @dagregi

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 3

Caution

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

⚠️ Outside diff range comments (4)
jans-cedarling/cedarling/src/http/cache_headers.rs (1)

59-62: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Parse all Cache-Control field-values, not just the first one.

HeaderMap::get() only sees a single Cache-Control entry, so a valid response like Cache-Control: max-age=600 plus a second Cache-Control: no-store will silently drop the later directive here. That breaks the refresh worker's cache-semantics contract by missing revalidation or freshness instructions. Please fold parse_cache_control() across headers.get_all(CACHE_CONTROL) and add a regression test with split header fields.

🤖 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 `@jans-cedarling/cedarling/src/http/cache_headers.rs` around lines 59 - 62, The
code currently uses headers.get(reqwest::header::CACHE_CONTROL) which only
returns one field-value and can drop subsequent Cache-Control headers; update
the logic that computes (cc_max_age, no_cache) to iterate
headers.get_all(reqwest::header::CACHE_CONTROL), convert each value with
to_str().ok(), and fold/accumulate the results by calling parse_cache_control
for each field-value and combining outcomes (preserve the most
restrictive/no-cache flag and the effective max-age per parse_cache_control
semantics) so directives from multiple header lines are honored; update the code
that produces cc_max_age and no_cache (the binding using parse_cache_control) to
use this folding approach and add a regression test that sends split
Cache-Control header fields (e.g., one line with max-age and another with
no-store/no-cache) to assert the combined parsed result is correct.
jans-cedarling/cedarling/src/init/policy_store_refresh.rs (3)

616-625: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Don't commit HEAD validators before the matching body is applied.

Lines 617-625 update state.validators before the GET/rebuild path succeeds. If that GET fails, the next HEAD returning the same validators will hit the validators_match() fast path and report NotModified, even though authz_swap is still serving the older policy set. Keep new_validators local and only write them into state after a successful GET / short-circuit on identical bytes. Based on PR objectives, load failures must keep the previously loaded policy store active, so refresh metadata needs to keep describing that active snapshot.

🤖 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 `@jans-cedarling/cedarling/src/init/policy_store_refresh.rs` around lines 616 -
625, Do not write new_validators into state before the GET/rebuild succeeds:
keep new_validators local, call tick_plain_get(ctx, state) (or a variant that
accepts new_validators) to fetch/rebuild using the local new_validators, and
only on successful GET/rebuild assign state.validators = new_validators and
reset state.consecutive_failures/record_helped; if the GET fails, leave
state.validators unchanged so future HEADs that match the currently active
snapshot still go through the rebuild path rather than returning
RefreshOutcome::NotModified. Ensure validators_match still short-circuits only
when comparing against state.validators, and move state.strategy.record_helped
calls to occur after success where appropriate.

151-176: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Start the reprobe cooldown when you downgrade the strategy.

choose_for_tick() treats last_probe_at == None as “probe now”. Because neither record_degraded() nor force_degrade() stamps last_probe_at, the first tick after every downgrade immediately retries Conditional instead of honoring STRATEGY_REPROBE_INTERVAL_SECS. That burns an extra request right after each downgrade and contradicts the “minimum elapsed time between attempts to upgrade back” behavior documented in this module.

Also applies to: 181-225

🤖 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 `@jans-cedarling/cedarling/src/init/policy_store_refresh.rs` around lines 151 -
176, When downgrading the strategy, ensure you start the reprobe cooldown by
recording the current time into StrategyState.last_probe_at so choose_for_tick()
will respect STRATEGY_REPROBE_INTERVAL_SECS; update both record_degraded() and
force_degrade() to set last_probe_at = Some(Utc::now()) (or the equivalent time
source used elsewhere) when they change current to a degraded strategy, leaving
choose_for_tick() unchanged so it will treat the downgrade as having just
probed.

1059-1277: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Add messages to the remaining bare assertions in this test block.

Several new assertions here still omit the required explanation (assert!(validators_match(&a, &b));, assert_eq!(s.current, ...), assert_ne!(body_hash(...), ...), etc.). Please add descriptive messages to the remaining bare assertions so failures explain which validator or strategy case regressed. As per coding guidelines, “All assertions must include a descriptive message explaining what is being tested”.

🤖 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 `@jans-cedarling/cedarling/src/init/policy_store_refresh.rs` around lines 1059
- 1277, Update the tests that use bare assertions to include descriptive failure
messages: for all validators_match tests (calls to validators_match(&a,&b)) add
messages explaining which validator case is expected (e.g., "ETag matches",
"Neither validator present", "ETag differs", "Last-Modified fallback", etc.),
for StrategyState tests (assert_eq/assert! on s.current, counters,
degraded_count, last_probe_at) include messages naming the strategy and expected
behavior (e.g., "strategy remains PlainGet after many degrades", "degrade
counter unchanged on choose_for_tick", "last_probe_at remains None when
Conditional"), and for body_hash tests (assert_eq/assert_ne) include messages
describing the hash property being tested (e.g., "empty bytes produce consistent
hash", "empty vs single-byte differ", "order-sensitive hash for 'ab' vs 'ba'").
Locate these assertions by the unique symbols validators_match, StrategyState,
RefreshStrategy, choose_for_tick, record_degraded, body_hash and replace the
bare assert/eq/ne calls with the same macros but passing a final string literal
message describing the specific expectation.

Source: Coding guidelines

🤖 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 `@jans-cedarling/cedarling/src/http/mod.rs`:
- Around line 255-261: The NotModified branch currently builds a brand-new
CacheHeadersState via CacheHeadersState::from_headers and returns it in
ConditionalFetch::NotModified, which discards any previously-known validators
(ETag/Last-Modified) when the 304 response omits fields; instead, merge the
incoming headers into the existing state.validators field: call a merging helper
(or update fields manually) to copy only non-empty/Some values from the new
CacheHeadersState into the prior state.validators so tick_conditional retains
existing ETag/Last-Modified and freshness metadata rather than replacing them
wholesale.
- Around line 584-587: The tests use bare assertions that violate the repo
style; update each assert!(matches!(result, super::ConditionalFetch::NotModified
{ .. })) and the related assert_eq! calls to include descriptive messages: for
the matches assertions (e.g., where result is matched against
super::ConditionalFetch::NotModified) add a message like "expected
ConditionalFetch::NotModified for <describe scenario> but got {:#?}", and for
assert_eq! include a message that prints both expected and actual (e.g.,
"refresh token invariants: expected X, got {:?}"). Apply these changes to the
three blocks shown (the NotModified matches around the result variable and the
assert_eq! pairs in the 621-627 and 658-665 sections) so each assertion includes
a clear, contextual message referencing the symbol names (result,
super::ConditionalFetch::NotModified, and the compared values).

In `@jans-cedarling/http_utils/src/lib.rs`:
- Around line 430-464: Replace the optional `if let Err(e) = bytes_result`
branch with a deterministic assertion that the read fails (use
`bytes_result.expect_err("expected mockito to truncate response and produce
DecodeResponseBytes")`), capture that error into `e`, then construct the
`HttpRequestError` via
`HttpRequestError::new(HttpRequestReasonError::DecodeResponseBytes(e),
Some(reqwest::StatusCode::OK))` and run the existing
`assert!(err.is_decode_error(), ...)` and related negative checks; this ensures
`bytes_result` actually exercises the `DecodeResponseBytes` path rather than
silently succeeding.

---

Outside diff comments:
In `@jans-cedarling/cedarling/src/http/cache_headers.rs`:
- Around line 59-62: The code currently uses
headers.get(reqwest::header::CACHE_CONTROL) which only returns one field-value
and can drop subsequent Cache-Control headers; update the logic that computes
(cc_max_age, no_cache) to iterate
headers.get_all(reqwest::header::CACHE_CONTROL), convert each value with
to_str().ok(), and fold/accumulate the results by calling parse_cache_control
for each field-value and combining outcomes (preserve the most
restrictive/no-cache flag and the effective max-age per parse_cache_control
semantics) so directives from multiple header lines are honored; update the code
that produces cc_max_age and no_cache (the binding using parse_cache_control) to
use this folding approach and add a regression test that sends split
Cache-Control header fields (e.g., one line with max-age and another with
no-store/no-cache) to assert the combined parsed result is correct.

In `@jans-cedarling/cedarling/src/init/policy_store_refresh.rs`:
- Around line 616-625: Do not write new_validators into state before the
GET/rebuild succeeds: keep new_validators local, call tick_plain_get(ctx, state)
(or a variant that accepts new_validators) to fetch/rebuild using the local
new_validators, and only on successful GET/rebuild assign state.validators =
new_validators and reset state.consecutive_failures/record_helped; if the GET
fails, leave state.validators unchanged so future HEADs that match the currently
active snapshot still go through the rebuild path rather than returning
RefreshOutcome::NotModified. Ensure validators_match still short-circuits only
when comparing against state.validators, and move state.strategy.record_helped
calls to occur after success where appropriate.
- Around line 151-176: When downgrading the strategy, ensure you start the
reprobe cooldown by recording the current time into StrategyState.last_probe_at
so choose_for_tick() will respect STRATEGY_REPROBE_INTERVAL_SECS; update both
record_degraded() and force_degrade() to set last_probe_at = Some(Utc::now())
(or the equivalent time source used elsewhere) when they change current to a
degraded strategy, leaving choose_for_tick() unchanged so it will treat the
downgrade as having just probed.
- Around line 1059-1277: Update the tests that use bare assertions to include
descriptive failure messages: for all validators_match tests (calls to
validators_match(&a,&b)) add messages explaining which validator case is
expected (e.g., "ETag matches", "Neither validator present", "ETag differs",
"Last-Modified fallback", etc.), for StrategyState tests (assert_eq/assert! on
s.current, counters, degraded_count, last_probe_at) include messages naming the
strategy and expected behavior (e.g., "strategy remains PlainGet after many
degrades", "degrade counter unchanged on choose_for_tick", "last_probe_at
remains None when Conditional"), and for body_hash tests (assert_eq/assert_ne)
include messages describing the hash property being tested (e.g., "empty bytes
produce consistent hash", "empty vs single-byte differ", "order-sensitive hash
for 'ab' vs 'ba'"). Locate these assertions by the unique symbols
validators_match, StrategyState, RefreshStrategy, choose_for_tick,
record_degraded, body_hash and replace the bare assert/eq/ne calls with the same
macros but passing a final string literal message describing the specific
expectation.
🪄 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: ASSERTIVE

Plan: Pro

Run ID: 2b61511b-b568-496d-a8c3-eccb7c5e05c9

📥 Commits

Reviewing files that changed from the base of the PR and between a40ddc2 and 1315130.

📒 Files selected for processing (4)
  • jans-cedarling/cedarling/src/http/cache_headers.rs
  • jans-cedarling/cedarling/src/http/mod.rs
  • jans-cedarling/cedarling/src/init/policy_store_refresh.rs
  • jans-cedarling/http_utils/src/lib.rs

Comment thread jans-cedarling/cedarling/src/http/mod.rs
Comment thread jans-cedarling/cedarling/src/http/mod.rs Outdated
Comment thread jans-cedarling/http_utils/src/lib.rs Outdated
logic

- Properly fold all `Cache-Control` header lines per RFC 9110.
- Introduce `merge_from` to ensure `304 Not Modified` responses only
  update cache headers rather than overwriting existing validators
  (ETag/
  Last-Modified).
- Fix `StrategyState` and `tick` logic to correctly handle transition
  cooldowns and prevent incorrect short-circuiting during HEAD requests.

Signed-off-by: haileyesus2433 <haileyesusbe@gmail.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

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

⚠️ Outside diff range comments (5)
jans-cedarling/cedarling/src/http/mod.rs (3)

791-792: 🧹 Nitpick | 🔵 Trivial | 💤 Low value

Add descriptive message to this assertion.

As per coding guidelines, "All assertions must include a descriptive message explaining what is being tested".

📝 Suggested fix
         let outcome = client.head_validators(&url).await.expect("request");
-        assert!(matches!(outcome, super::HeadOutcome::NotSupported));
+        assert!(
+            matches!(outcome, super::HeadOutcome::NotSupported),
+            "HEAD returning 405 must map to HeadOutcome::NotSupported",
+        );
🤖 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 `@jans-cedarling/cedarling/src/http/mod.rs` around lines 791 - 792, The test
assertion using assert!(matches!(outcome, super::HeadOutcome::NotSupported))
lacks a descriptive message; update the assert! call for
HeadOutcome::NotSupported to include a clear message explaining what is being
verified (e.g., that the outcome equals HeadOutcome::NotSupported and why),
referencing the HeadOutcome::NotSupported enum variant and the local variable
outcome so reviewers can quickly understand the intent of the assertion.

Source: Coding guidelines


808-809: 🧹 Nitpick | 🔵 Trivial | 💤 Low value

Add descriptive message to this assertion.

As per coding guidelines, "All assertions must include a descriptive message explaining what is being tested".

📝 Suggested fix
         let outcome = client.head_validators(&url).await.expect("request");
-        assert!(matches!(outcome, super::HeadOutcome::NotSupported));
+        assert!(
+            matches!(outcome, super::HeadOutcome::NotSupported),
+            "HEAD returning 501 must map to HeadOutcome::NotSupported",
+        );
🤖 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 `@jans-cedarling/cedarling/src/http/mod.rs` around lines 808 - 809, The
assertion on the head_validators result lacks a descriptive message; update the
assert! that checks matches!(outcome, super::HeadOutcome::NotSupported) to
include a clear failure message that states the expectation and prints the
actual outcome (e.g., "expected HeadOutcome::NotSupported but got {:?}",
outcome) so failures are informative; modify the assertion around the variable
outcome returned by client.head_validators(...) accordingly.

Source: Coding guidelines


738-748: 🧹 Nitpick | 🔵 Trivial | 💤 Low value

Add descriptive messages to these HEAD validator test assertions.

These assertions lack the descriptive messages required by coding guidelines.

As per coding guidelines, "All assertions must include a descriptive message explaining what is being tested".

📝 Suggested fix
         match outcome {
             super::HeadOutcome::Headers(v) => {
-                assert_eq!(v.etag.as_deref(), Some("\"v3\""));
+                assert_eq!(
+                    v.etag.as_deref(),
+                    Some("\"v3\""),
+                    "HEAD response ETag must be captured",
+                );
                 assert_eq!(
                     v.last_modified.as_deref(),
-                    Some("Sun, 22 May 2026 12:00:00 GMT")
+                    Some("Sun, 22 May 2026 12:00:00 GMT"),
+                    "HEAD response Last-Modified must be captured",
                 );
-                assert!(v.has_validator());
+                assert!(
+                    v.has_validator(),
+                    "has_validator() must be true when ETag and Last-Modified are present",
+                );
             },
🤖 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 `@jans-cedarling/cedarling/src/http/mod.rs` around lines 738 - 748, The test's
assertions inside the HeadOutcome::Headers match arm lack descriptive messages;
update each assert_eq! and assert! (the checks on v.etag.as_deref(),
v.last_modified.as_deref(), and v.has_validator()) to include a clear message
string describing what is being validated (for example: "expected ETag to be
\"v3\"", "expected Last-Modified to be ...", "expected validator to be
present"), and similarly update the panic path for HeadOutcome::NotSupported to
include context like "expected Headers, got NotSupported"; ensure the
surrounding test flow (including mock.assert_async().await) remains unchanged.

Source: Coding guidelines

jans-cedarling/http_utils/src/lib.rs (1)

452-461: 🧹 Nitpick | 🔵 Trivial | 💤 Low value

Add descriptive messages to cross-check assertions.

These assertions verify the negative/other-variant behavior but lack descriptive messages.

As per coding guidelines, "All assertions must include a descriptive message explaining what is being tested".

📝 Suggested fix
         // Cross-check the negative directions on stable error variants.
         let max_retries =
             HttpRequestError::new(HttpRequestReasonError::MaxRetriesExceeded, None);
-        assert!(!max_retries.is_decode_error());
-        assert!(max_retries.is_max_retries_exceeded());
+        assert!(
+            !max_retries.is_decode_error(),
+            "MaxRetriesExceeded must NOT satisfy is_decode_error()",
+        );
+        assert!(
+            max_retries.is_max_retries_exceeded(),
+            "MaxRetriesExceeded must satisfy is_max_retries_exceeded()",
+        );

         let status =
             HttpRequestError::new(HttpRequestReasonError::HttpStatusError, None);
-        assert!(!status.is_decode_error());
-        assert!(!status.is_max_retries_exceeded());
-        assert!(status.is_http_status_error());
+        assert!(
+            !status.is_decode_error(),
+            "HttpStatusError must NOT satisfy is_decode_error()",
+        );
+        assert!(
+            !status.is_max_retries_exceeded(),
+            "HttpStatusError must NOT satisfy is_max_retries_exceeded()",
+        );
+        assert!(
+            status.is_http_status_error(),
+            "HttpStatusError must satisfy is_http_status_error()",
+        );
🤖 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 `@jans-cedarling/http_utils/src/lib.rs` around lines 452 - 461, The assertions
around HttpRequestError::new using HttpRequestReasonError::MaxRetriesExceeded
and HttpRequestReasonError::HttpStatusError lack descriptive messages; update
each assert! (the ones calling is_decode_error(), is_max_retries_exceeded(), and
is_http_status_error()) to include a clear string describing the expectation
(e.g., "MaxRetriesExceeded should not be a decode error", "MaxRetriesExceeded
should be max retries exceeded", "HttpStatusError should not be decode error",
"HttpStatusError should not be max retries exceeded", "HttpStatusError should be
http status error") so each check on the HttpRequestError::new results contains
an explanatory message.

Source: Coding guidelines

jans-cedarling/cedarling/src/init/policy_store_refresh.rs (1)

1081-1081: 🧹 Nitpick | 🔵 Trivial | 💤 Low value

Add descriptive message to this assertion.

As per coding guidelines, "All assertions must include a descriptive message explaining what is being tested".

📝 Suggested fix
-        assert!(validators_match(&a, &b), "matching ETag wins");
+        assert!(
+            validators_match(&a, &b),
+            "when both sides have ETag, ETag match takes precedence over differing Last-Modified",
+        );
🤖 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 `@jans-cedarling/cedarling/src/init/policy_store_refresh.rs` at line 1081, The
assertion assert!(validators_match(&a, &b), "matching ETag wins"); lacks a
descriptive message; update it to include what is being validated and expected
outcome (e.g., explain that two validator sets should be equal when ETags
match), by replacing the lightweight message with a clear description
referencing validators_match, the inputs (&a, &b) and that matching ETag implies
identical validators so failures show those details for debugging.

Source: Coding guidelines

🤖 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 `@jans-cedarling/cedarling/src/init/policy_store_refresh.rs`:
- Around line 1232-1233: The two assertions asserting
s.conditional_to_head_transitions == 0 and s.head_to_plain_transitions == 0 lack
descriptive messages; update the assert_eq! calls in the test (where the
variable s is inspected in policy_store_refresh.rs) to include descriptive
messages that state what is being tested and show expected vs actual (e.g.,
"conditional_to_head_transitions should be 0" and "head_to_plain_transitions
should be 0") so failures are self-explanatory; keep the assertions on the same
symbols s.conditional_to_head_transitions and s.head_to_plain_transitions and
only add the message argument to each assert_eq! macro.

---

Outside diff comments:
In `@jans-cedarling/cedarling/src/http/mod.rs`:
- Around line 791-792: The test assertion using assert!(matches!(outcome,
super::HeadOutcome::NotSupported)) lacks a descriptive message; update the
assert! call for HeadOutcome::NotSupported to include a clear message explaining
what is being verified (e.g., that the outcome equals HeadOutcome::NotSupported
and why), referencing the HeadOutcome::NotSupported enum variant and the local
variable outcome so reviewers can quickly understand the intent of the
assertion.
- Around line 808-809: The assertion on the head_validators result lacks a
descriptive message; update the assert! that checks matches!(outcome,
super::HeadOutcome::NotSupported) to include a clear failure message that states
the expectation and prints the actual outcome (e.g., "expected
HeadOutcome::NotSupported but got {:?}", outcome) so failures are informative;
modify the assertion around the variable outcome returned by
client.head_validators(...) accordingly.
- Around line 738-748: The test's assertions inside the HeadOutcome::Headers
match arm lack descriptive messages; update each assert_eq! and assert! (the
checks on v.etag.as_deref(), v.last_modified.as_deref(), and v.has_validator())
to include a clear message string describing what is being validated (for
example: "expected ETag to be \"v3\"", "expected Last-Modified to be ...",
"expected validator to be present"), and similarly update the panic path for
HeadOutcome::NotSupported to include context like "expected Headers, got
NotSupported"; ensure the surrounding test flow (including
mock.assert_async().await) remains unchanged.

In `@jans-cedarling/cedarling/src/init/policy_store_refresh.rs`:
- Line 1081: The assertion assert!(validators_match(&a, &b), "matching ETag
wins"); lacks a descriptive message; update it to include what is being
validated and expected outcome (e.g., explain that two validator sets should be
equal when ETags match), by replacing the lightweight message with a clear
description referencing validators_match, the inputs (&a, &b) and that matching
ETag implies identical validators so failures show those details for debugging.

In `@jans-cedarling/http_utils/src/lib.rs`:
- Around line 452-461: The assertions around HttpRequestError::new using
HttpRequestReasonError::MaxRetriesExceeded and
HttpRequestReasonError::HttpStatusError lack descriptive messages; update each
assert! (the ones calling is_decode_error(), is_max_retries_exceeded(), and
is_http_status_error()) to include a clear string describing the expectation
(e.g., "MaxRetriesExceeded should not be a decode error", "MaxRetriesExceeded
should be max retries exceeded", "HttpStatusError should not be decode error",
"HttpStatusError should not be max retries exceeded", "HttpStatusError should be
http status error") so each check on the HttpRequestError::new results contains
an explanatory message.
🪄 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: ASSERTIVE

Plan: Pro

Run ID: d6f0abba-a905-496f-81aa-8dd87013ccba

📥 Commits

Reviewing files that changed from the base of the PR and between 1315130 and 71ab100.

📒 Files selected for processing (4)
  • jans-cedarling/cedarling/src/http/cache_headers.rs
  • jans-cedarling/cedarling/src/http/mod.rs
  • jans-cedarling/cedarling/src/init/policy_store_refresh.rs
  • jans-cedarling/http_utils/src/lib.rs

Comment thread jans-cedarling/cedarling/src/init/policy_store_refresh.rs Outdated
@haileyesus2433

Copy link
Copy Markdown
Contributor Author

@coderabbitai if no more comments approve

@coderabbitai

coderabbitai Bot commented Jun 9, 2026

Copy link
Copy Markdown
Contributor

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check the open review comments on PR `#14174`
gh pr view 14174 --repo JanssenProject/jans --json reviewDecision,reviews,state
echo "---"
# List review threads/comments
gh api repos/JanssenProject/jans/pulls/14174/reviews --jq '.[] | {id: .id, user: .user.login, state: .state, submitted_at: .submitted_at}'
echo "---"
# Check for unresolved review comments from coderabbitai
gh api repos/JanssenProject/jans/pulls/14174/comments --jq '.[] | select(.user.login == "coderabbitai[bot]") | {id: .id, body: .body[:100], in_reply_to_id: .in_reply_to_id}'

Length of output: 133902



@haileyesus2433 There are still open review comments from the last two review rounds that need to be addressed before I can approve. Here's a summary of the outstanding items:

From the latest reviews (2026-06-09), not yet resolved:

🟠 Major — outside diff (need fixes before approval):

  1. no-store freshness not zeroed (cache_headers.rs ~line 120–142): Cache-Control: no-store, max-age=600 still produces fresh_for=600 instead of forcing revalidation — no-store must be treated the same as no-cache in parse_cache_control.
  2. HEAD validators committed before GET/rebuild succeeds (policy_store_refresh.rs ~line 616–625): state.validators is updated with new_validators before the downstream GET/rebuild path completes. If the GET fails, future HEADs will incorrectly short-circuit as NotModified while the older policy set is still active.
  3. Reprobe cooldown not started on downgrade (policy_store_refresh.rs ~line 151–176): Neither record_degraded() nor force_degrade() stamps last_probe_at, so the first tick after every downgrade immediately retries Conditional instead of waiting STRATEGY_REPROBE_INTERVAL_SECS.

🔵 Trivial (nits — missing descriptive assertion messages):

  • Several assert!/assert_eq!/assert_ne! calls in tests across http/mod.rs, http_utils/src/lib.rs, and policy_store_refresh.rs still lack the required descriptive messages per the project's coding guidelines.

Once those major items are addressed, I'm happy to approve. The trivial nits can be done in the same pass or filed as follow-up work if preferred.

dagregi
dagregi previously approved these changes Jun 9, 2026
@haileyesus2433

Copy link
Copy Markdown
Contributor Author

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain
🏁 Script executed:

#!/bin/bash
# Check the open review comments on PR `#14174`
gh pr view 14174 --repo JanssenProject/jans --json reviewDecision,reviews,state
echo "---"
# List review threads/comments
gh api repos/JanssenProject/jans/pulls/14174/reviews --jq '.[] | {id: .id, user: .user.login, state: .state, submitted_at: .submitted_at}'
echo "---"
# Check for unresolved review comments from coderabbitai
gh api repos/JanssenProject/jans/pulls/14174/comments --jq '.[] | select(.user.login == "coderabbitai[bot]") | {id: .id, body: .body[:100], in_reply_to_id: .in_reply_to_id}'

Length of output: 133902

@haileyesus2433 There are still open review comments from the last two review rounds that need to be addressed before I can approve. Here's a summary of the outstanding items:

From the latest reviews (2026-06-09), not yet resolved:

🟠 Major — outside diff (need fixes before approval):

  1. no-store freshness not zeroed (cache_headers.rs ~line 120–142): Cache-Control: no-store, max-age=600 still produces fresh_for=600 instead of forcing revalidation — no-store must be treated the same as no-cache in parse_cache_control.
  2. HEAD validators committed before GET/rebuild succeeds (policy_store_refresh.rs ~line 616–625): state.validators is updated with new_validators before the downstream GET/rebuild path completes. If the GET fails, future HEADs will incorrectly short-circuit as NotModified while the older policy set is still active.
  3. Reprobe cooldown not started on downgrade (policy_store_refresh.rs ~line 151–176): Neither record_degraded() nor force_degrade() stamps last_probe_at, so the first tick after every downgrade immediately retries Conditional instead of waiting STRATEGY_REPROBE_INTERVAL_SECS.

🔵 Trivial (nits — missing descriptive assertion messages):

  • Several assert!/assert_eq!/assert_ne! calls in tests across http/mod.rs, http_utils/src/lib.rs, and policy_store_refresh.rs still lack the required descriptive messages per the project's coding guidelines.

Once those major items are addressed, I'm happy to approve. The trivial nits can be done in the same pass or filed as follow-up work if preferred.

@coderabbitai all of those are addresed from commit c6c21ed - 71ab100

@coderabbitai

coderabbitai Bot commented Jun 9, 2026

Copy link
Copy Markdown
Contributor

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check the relevant sections in cache_headers.rs for no-store handling
rg -n "no_store|no-store|no_cache|fresh_for" jans-cedarling/cedarling/src/http/cache_headers.rs | head -60

echo "=== StrategyState / reprobe / last_probe_at ==="
rg -n "last_probe_at|record_degraded|force_degrade|STRATEGY_REPROBE" jans-cedarling/cedarling/src/init/policy_store_refresh.rs | head -60

echo "=== HEAD validators commit logic ==="
rg -n "new_validators|state\.validators" jans-cedarling/cedarling/src/init/policy_store_refresh.rs | head -60

Length of output: 7238



@haileyesus2433 I've verified the fixes across the commits:

  1. no-store freshness zeroed ✅ — parse_cache_control now folds no-store into the no_cache flag (line 178), and no_cache = true forces fresh_for = Duration::ZERO (line 116). New tests at lines 286–319 cover the no-store, max-age=600 case explicitly.

  2. HEAD validators not committed before GET/rebuild ✅ — state.validators.merge_from(new_validators) at line 630 only fires on the validators_match short-circuit path. The comment at line 636 confirms validators are intentionally withheld until parse_swap_and_record succeeds (line 724).

  3. Reprobe cooldown stamped on downgrade ✅ — Both record_degraded() (lines 197, 203) and force_degrade() (lines 224, 230) now stamp self.last_probe_at = Some(Utc::now()), so the reprobe interval is enforced immediately after any downgrade.

Everything looks good — approving! 🐇 ✨

[approve]

Signed-off-by: haileyesus2433 <haileyesusbe@gmail.com>
Introduce `read_response_capped` to the `HttpClient` to ensure that all
policy store fetches and refresh ticks honor the configured
`max_response_size_bytes` limit.

Signed-off-by: haileyesus2433 <haileyesusbe@gmail.com>
Extract error classification logic into `classify_fetch_error` to
improve
testability and fix a bug where retried HTTP errors were misidentified
as
network errors. Added `status_code` accessor to `HttpRequestError` to
ensure errors containing HTTP responses are correctly categorized even
after retry exhaustion.

Signed-off-by: haileyesus2433 <haileyesusbe@gmail.com>
store swap

Ensure the metrics gauge reflects the current policy count after a
successful refresh. Previously, it remained pinned to the bootstrap
value, causing the reported metric to diverge from the active policy
set.

Signed-off-by: haileyesus2433 <haileyesusbe@gmail.com>
headers

Ensure `fresh_for` is overwritten by incoming values (including `None`)
to prevent old `max-age` values from pinning the worker refresh cadence
indefinitely when subsequent 304 responses provide no new freshness
claims. Maintain sticky behavior for resource validators.

Signed-off-by: haileyesus2433 <haileyesusbe@gmail.com>
validators

Replace validator overwriting with a merge operation during body hash
short
circuits to prevent the loss of cache validation headers when a server
or
proxy omits them in subsequent responses.

Signed-off-by: haileyesus2433 <haileyesusbe@gmail.com>
Consolidate the degradation logic into a single source of truth,
`degrade_one_step`, to ensure consistent transition handling, counter
updates, and cooldown stamping across both automatic and forced
degradations.

Signed-off-by: haileyesus2433 <haileyesusbe@gmail.com>
Compare ETags using weak comparison semantics by stripping the optional
`W/` prefix. This prevents unnecessary full GET requests when a server
returns a weak ETag in a HEAD probe but a strong ETag in a subsequent
GET response.

Signed-off-by: haileyesus2433 <haileyesusbe@gmail.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Caution

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

⚠️ Outside diff range comments (2)
jans-cedarling/cedarling/src/http/cache_headers.rs (1)

189-194: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Make repeated max-age directives min-win even inside one header value.

parse_cache_control("max-age=60, max-age=600") currently returns 600 because the later directive overwrites the earlier one. That disagrees with the “most restrictive wins” merge you already apply across separate Cache-Control lines, and intermediaries are allowed to coalesce repeated header lines into one comma-separated value. The same response can therefore produce a different refresh cadence depending on wire formatting.

Suggested fix
         } else if let Some(rest) = lower.strip_prefix("max-age=") {
             // Strip optional quotes; ignore unparseable / negative values.
             let trimmed = rest.trim().trim_matches('"');
             if let Ok(secs) = trimmed.parse::<u64>() {
-                max_age = Some(secs);
+                max_age = Some(match max_age {
+                    Some(prev) => prev.min(secs),
+                    None => secs,
+                });
             }
         }
🤖 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 `@jans-cedarling/cedarling/src/http/cache_headers.rs` around lines 189 - 194,
The max-age parsing branch currently overwrites any previous max_age when
encountering another "max-age=" token in the same header value; modify the block
in cache_headers.rs (the branch using lower.strip_prefix("max-age=") that sets
max_age) to take the minimum of the existing max_age and the newly parsed secs
instead of replacing it: if max_age is None set Some(secs), otherwise set
Some(existing.min(secs)); keep the same parsing/trim logic and error handling.
jans-cedarling/http_utils/src/lib.rs (1)

73-95: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Expose oversized-body failures as body-read errors too.

read_response_capped() reports cap breaches as ResponseTooLarge, but is_decode_error() only matches DecodeResponseBytes(_). Downstream, that makes an oversized conditional GET surface as HttpError via classify_fetch_error(), while tick_plain_get() records the same over-cap body as DecodeError. The outcome bucket now depends on strategy, not failure mode.

Please broaden this helper or add a separate is_body_read_error() that covers both DecodeResponseBytes(_) and ResponseTooLarge, then use that in the refresh worker’s classifier.

🤖 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 `@jans-cedarling/http_utils/src/lib.rs` around lines 73 - 95, The helper is
currently only matching HttpRequestReasonError::DecodeResponseBytes in
is_decode_error(), so oversized-body failures (ResponseTooLarge) are not
classified as body-read errors; add a new helper is_body_read_error() (or
broaden is_decode_error()) that returns true for both
HttpRequestReasonError::DecodeResponseBytes(_) and
HttpRequestReasonError::ResponseTooLarge, and update callers (e.g., the refresh
worker classifier) to use the new is_body_read_error() so capped-response errors
and decode errors are treated the same.
🤖 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.

Outside diff comments:
In `@jans-cedarling/cedarling/src/http/cache_headers.rs`:
- Around line 189-194: The max-age parsing branch currently overwrites any
previous max_age when encountering another "max-age=" token in the same header
value; modify the block in cache_headers.rs (the branch using
lower.strip_prefix("max-age=") that sets max_age) to take the minimum of the
existing max_age and the newly parsed secs instead of replacing it: if max_age
is None set Some(secs), otherwise set Some(existing.min(secs)); keep the same
parsing/trim logic and error handling.

In `@jans-cedarling/http_utils/src/lib.rs`:
- Around line 73-95: The helper is currently only matching
HttpRequestReasonError::DecodeResponseBytes in is_decode_error(), so
oversized-body failures (ResponseTooLarge) are not classified as body-read
errors; add a new helper is_body_read_error() (or broaden is_decode_error())
that returns true for both HttpRequestReasonError::DecodeResponseBytes(_) and
HttpRequestReasonError::ResponseTooLarge, and update callers (e.g., the refresh
worker classifier) to use the new is_body_read_error() so capped-response errors
and decode errors are treated the same.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 8f2c4807-c5c6-4d88-82de-4d9d4627cd2c

📥 Commits

Reviewing files that changed from the base of the PR and between 71ab100 and 2a06bf7.

📒 Files selected for processing (6)
  • jans-cedarling/cedarling/src/authz/metrics.rs
  • jans-cedarling/cedarling/src/http/cache_headers.rs
  • jans-cedarling/cedarling/src/http/mod.rs
  • jans-cedarling/cedarling/src/init/policy_store.rs
  • jans-cedarling/cedarling/src/init/policy_store_refresh.rs
  • jans-cedarling/http_utils/src/lib.rs

@olehbozhok

olehbozhok commented Jun 10, 2026

Copy link
Copy Markdown
Contributor

Review: 4 findings

Checked each against current main. All items that affected correctness (unbounded body read, error misclassification, stale policy_count gauge, sticky fresh_for, validator overwrite, force_degrade duplication, weak ETag comparison) are fixed. What remains is correctness-of-observability and maintenance debt.


1. choose_for_tick skips HeadThenGet as an intermediate upgrade step

Severity: Low (narrow scenario, no data loss)

What: choose_for_tick() (policy_store_refresh.rs:153) has two arms: when current == Conditional, stay put; otherwise probe Conditional after STRATEGY_REPROBE_INTERVAL_SECS (1800s). There is no "probe HeadThenGet first" arm. A worker at PlainGet must jump directly to a full conditional GET (body download included) to test whether the upstream has recovered; it never tries the lighter HeadThenGet probe.

Failure scenario: Upstream supports ETag/Last-Modified but ignores If-None-Match (returns 200 with the same body instead of 304). The conditional GET downloads the full body, the hash matches, and the short-circuit kicks in with is_probe=true, so neither record_degraded() nor record_probe_success() fires. current stays PlainGet. The worker retries after 1800s — same result forever. A HeadThenGet probe (cheap HEAD, no body) would have shown the validators match, triggering record_helped() and upgrading to HeadThenGet.

Suggestion: In choose_for_tick(), when current == PlainGet, probe HeadThenGet (not Conditional). Only probe Conditional from HeadThenGet. This gives a two-step upgrade: PlainGet → HeadThenGet → Conditional, matching the degradation ladder.


2. AuthzRebuilder::rebuild unconditionally re-fetches JWKS on every refresh

Severity: Medium (unnecessary HTTP traffic + background task churn on every policy change)

What: rebuild() (policy_store_refresh.rs:257) calls JwtService::new(...) on every invocation, which performs OIDC discovery + JWKS fetch for every trusted issuer, then spawns per-issuer background pollers (keep_jwks_updated) and a token-cache cleanup task. This happens on every policy-store refresh even when the trusted_issuers set is byte-identical to the currently active one.

Impact: A policy-only edit (Cedar rules change, but trusted_issuers unchanged) triggers N OIDC + JWKS HTTP fetches plus N+1 new background tasks on every refresh tick. Old Arc<JwtService> instances survive until the old Arc<Authz> is reclaimed, so multiple JWKS-polling tasks coexist for the same issuer during the overlap window. On a short refresh interval (<60s) against a frequently-changing policy body, this multiplies background task count linearly.

Suggestion: Pass current_authz: Option<&Authz> to rebuild(). When the new trusted_issuers deeply-equals the current one, clone the existing Arc<JwtService> instead of constructing a new one.


3. rebuild duplicates ServiceFactory::authz_service pipeline, drops the default-entities warn loop

Severity: Low (silent log omission; drift risk on future changes)

What: rebuild() (policy_store_refresh.rs:261–296) re-implements the same construction steps as ServiceFactory::authz_service + entity_builder + jwt_service (service_factory.rs:62–171) but omits the default_entities.warns() log loop (service_factory.rs:125–136). Any future change to the startup construction (new validation step, new config field) must be manually mirrored here, or the refresh path silently builds a differently-configured Authz than the one used at startup.

Suggestion: Extract a shared fn build_authz(policy_store, jwt_config, ...) -> Result<Authz, ...> called by both ServiceFactory and AuthzRebuilder.


4. 14 policy_store_refresh_* atomics hand-listed in 3+ places with no compiler guard

Severity: Low (maintenance hazard — omission compiles silently)

What: The 14 atomic counters are declared as individual fields in MetricsCollector (metrics.rs:284–309), zero-initialised in both MetricsCollector::new() and ::disabled(), and re-listed in inject_policy_store_refresh_state() for snapshot emission. Adding or removing a counter requires identical edits in all three locations (plus the record_policy_store_refresh match); an omission compiles silently — zeros either never get set or the snapshot silently drops a field.

Suggestion: Extract a #[derive(Default)] PolicyStoreRefreshMetrics sub-struct with one fn inject(...) and one fn record_outcome(...). Replace the 14 inline fields with a single policy_store_refresh: PolicyStoreRefreshMetrics in MetricsCollector.

@haileyesus2433

Copy link
Copy Markdown
Contributor Author

Review: 4 findings

Checked each against current main. All items that affected correctness (unbounded body read, error misclassification, stale policy_count gauge, sticky fresh_for, validator overwrite, force_degrade duplication, weak ETag comparison) are fixed. What remains is correctness-of-observability and maintenance debt.

1. choose_for_tick skips HeadThenGet as an intermediate upgrade step

Severity: Low (narrow scenario, no data loss)

What: choose_for_tick() (policy_store_refresh.rs:153) has two arms: when current == Conditional, stay put; otherwise probe Conditional after STRATEGY_REPROBE_INTERVAL_SECS (1800s). There is no "probe HeadThenGet first" arm. A worker at PlainGet must jump directly to a full conditional GET (body download included) to test whether the upstream has recovered; it never tries the lighter HeadThenGet probe.

Failure scenario: Upstream supports ETag/Last-Modified but ignores If-None-Match (returns 200 with the same body instead of 304). The conditional GET downloads the full body, the hash matches, and the short-circuit kicks in with is_probe=true, so neither record_degraded() nor record_probe_success() fires. current stays PlainGet. The worker retries after 1800s — same result forever. A HeadThenGet probe (cheap HEAD, no body) would have shown the validators match, triggering record_helped() and upgrading to HeadThenGet.

Suggestion: In choose_for_tick(), when current == PlainGet, probe HeadThenGet (not Conditional). Only probe Conditional from HeadThenGet. This gives a two-step upgrade: PlainGet → HeadThenGet → Conditional, matching the degradation ladder.

2. AuthzRebuilder::rebuild unconditionally re-fetches JWKS on every refresh

Severity: Medium (unnecessary HTTP traffic + background task churn on every policy change)

What: rebuild() (policy_store_refresh.rs:257) calls JwtService::new(...) on every invocation, which performs OIDC discovery + JWKS fetch for every trusted issuer, then spawns per-issuer background pollers (keep_jwks_updated) and a token-cache cleanup task. This happens on every policy-store refresh even when the trusted_issuers set is byte-identical to the currently active one.

Impact: A policy-only edit (Cedar rules change, but trusted_issuers unchanged) triggers N OIDC + JWKS HTTP fetches plus N+1 new background tasks on every refresh tick. Old Arc<JwtService> instances survive until the old Arc<Authz> is reclaimed, so multiple JWKS-polling tasks coexist for the same issuer during the overlap window. On a short refresh interval (<60s) against a frequently-changing policy body, this multiplies background task count linearly.

Suggestion: Pass current_authz: Option<&Authz> to rebuild(). When the new trusted_issuers deeply-equals the current one, clone the existing Arc<JwtService> instead of constructing a new one.

3. rebuild duplicates ServiceFactory::authz_service pipeline, drops the default-entities warn loop

Severity: Low (silent log omission; drift risk on future changes)

What: rebuild() (policy_store_refresh.rs:261–296) re-implements the same construction steps as ServiceFactory::authz_service + entity_builder + jwt_service (service_factory.rs:62–171) but omits the default_entities.warns() log loop (service_factory.rs:125–136). Any future change to the startup construction (new validation step, new config field) must be manually mirrored here, or the refresh path silently builds a differently-configured Authz than the one used at startup.

Suggestion: Extract a shared fn build_authz(policy_store, jwt_config, ...) -> Result<Authz, ...> called by both ServiceFactory and AuthzRebuilder.

4. 14 policy_store_refresh_* atomics hand-listed in 3+ places with no compiler guard

Severity: Low (maintenance hazard — omission compiles silently)

What: The 14 atomic counters are declared as individual fields in MetricsCollector (metrics.rs:284–309), zero-initialised in both MetricsCollector::new() and ::disabled(), and re-listed in inject_policy_store_refresh_state() for snapshot emission. Adding or removing a counter requires identical edits in all three locations (plus the record_policy_store_refresh match); an omission compiles silently — zeros either never get set or the snapshot silently drops a field.

Suggestion: Extract a #[derive(Default)] PolicyStoreRefreshMetrics sub-struct with one fn inject(...) and one fn record_outcome(...). Replace the 14 inline fields with a single policy_store_refresh: PolicyStoreRefreshMetrics in MetricsCollector.

i have created a separate issue for all of them in under improvements #14245 #14244 #14243 #14242

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area-documentation Documentation needs to change as part of issue or PR comp-docs Touching folder /docs comp-jans-cedarling Touching folder /jans-cedarling kind-feature Issue or PR is a new feature request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(jans-cedarling): updating policies from URL by ttl

5 participants