Skip to content

Add read-only mode to reject write operations#230

Merged
em3s merged 14 commits intomainfrom
feat/read-only-mode
Apr 1, 2026
Merged

Add read-only mode to reject write operations#230
em3s merged 14 commits intomainfrom
feat/read-only-mode

Conversation

@eazyhozy
Copy link
Copy Markdown
Member

@eazyhozy eazyhozy commented Mar 27, 2026

Summary

Add a read-only mode that rejects write operations at the WebFilter level.

Closes #229

Changes

  • Add readOnly property to ServerProperties (actionbase.read-only, default: false)
  • Add ReadOnlyRequestFilter that blocks non-GET methods on /graph/v2 and /graph/v3 paths
  • Allowlist read-only POST endpoints (/edges/get, /multi-edges/ids, /query)
  • Register filter at Order(1), before MirrorRequestFilter(2) and TokenAuthenticationFilter(3)

Usage

To enable read-only mode, set actionbase.read-only in the application profile:

actionbase:
  read-only: true

Or, to inject via environment variable (e.g. K8s manifest):

actionbase:
  read-only: ${AB_READ_ONLY:false}

No configuration is needed to keep the default behavior (read-only disabled).

How to Test

  • ./gradlew :server:test --tests "*ReadOnlyRequestFilterTest*" — unit tests
  • ./gradlew :server:test --tests "*StartUp*" — E2E startup tests
  • ./gradlew :server:test — full regression

@eazyhozy eazyhozy self-assigned this Mar 27, 2026
@eazyhozy eazyhozy marked this pull request as ready for review March 27, 2026 02:33
@eazyhozy eazyhozy requested a review from em3s as a code owner March 27, 2026 02:33
@dosubot dosubot Bot added size:L This PR changes 100-499 lines, ignoring generated files. enhancement New feature or request labels Mar 27, 2026
@eazyhozy
Copy link
Copy Markdown
Member Author

@em3s Thanks for the feedback on #229! When you get a chance, could you take a look at the PR as well? 🙏

import org.springframework.test.context.TestPropertySource

@TestPropertySource(properties = ["actionbase.read-only=true"])
class ReadOnlyModeStartUpTest : E2ETestBase() {
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.

We should also cover the case actionbase.read-only=false, even though other tests cover it implicitly.

Copy link
Copy Markdown
Member Author

@eazyhozy eazyhozy Mar 30, 2026

Choose a reason for hiding this comment

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

Done in c1fcea1. Added StartUpWithReadOnlyDisabledTest (read-only=false) and a default case test in StartUpTest (read-only not configured).


@Test
fun `should allow GET requests on graph v2 paths`() {
val exchange = MockServerWebExchange.from(MockServerHttpRequest.get("/graph/v2/service/s/label/l/edge"))
Copy link
Copy Markdown
Contributor

@em3s em3s Mar 30, 2026

Choose a reason for hiding this comment

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

Please define ALL paths as constants like READ_PATHS = ..., WRITE_PATHS = ... and consolidate the test cases.

Copy link
Copy Markdown
Member Author

@eazyhozy eazyhozy Mar 30, 2026

Choose a reason for hiding this comment

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

@em3s
Defined READ_PATHS / WRITE_PATHS constants and consolidated with @ObjectSourceParameterizedTest (16efa39). Should these constants live in production ReadOnlyRequestFilter? If so, I would switch to @MethodSource since @ObjectSource YAML cannot reference Kotlin constants.

@eazyhozy eazyhozy changed the title feat(server): add read-only mode to reject write operations Add read-only mode to reject write operations Mar 30, 2026
@em3s
Copy link
Copy Markdown
Contributor

em3s commented Mar 30, 2026

Reviewed. Please address the comments. @eazyhozy

@eazyhozy
Copy link
Copy Markdown
Member Author

@em3s Addressed all 4 feedback items. Ready for re-review.

private val log = LoggerFactory.getLogger(ReadOnlyRequestFilter::class.java)

private val paths = setOf("/graph/v2", "/graph/v3")
private val readMethods = setOf(HttpMethod.GET, HttpMethod.HEAD, HttpMethod.OPTIONS)
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.

We only use GET.

private val readMethods = setOf(HttpMethod.GET) or     private val readMethod = HttpMethod.GET

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Done in cef984e.


@ObjectSourceParameterizedTest
@ObjectSource(
"""
Copy link
Copy Markdown
Contributor

@em3s em3s Mar 30, 2026

Choose a reason for hiding this comment

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

split into get and post cases. (means two tests)
and please add all paths.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Done in cef984e. Split into should allow GET requests on graph paths and should allow read-only POST requests. Added scan, count, counts, agg paths.

path: /graph/v3/databases/db/tables/t
- method: DELETE
path: /graph/v2/admin/service/test
- method: PATCH
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.

Do we have PATCH?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

No. Removed in cef984e.

@em3s
Copy link
Copy Markdown
Contributor

em3s commented Mar 30, 2026

Please check the updates.

@eazyhozy
Copy link
Copy Markdown
Member Author

eazyhozy commented Mar 31, 2026

@em3s All feedback addressed, including constants — added exhaustive endpoint coverage for all v2/v3 paths (926a8b0). Ready for re-review.

@dosubot dosubot Bot added size:XL This PR changes 500-999 lines, ignoring generated files. and removed size:L This PR changes 100-499 lines, ignoring generated files. labels Mar 31, 2026
@eazyhozy
Copy link
Copy Markdown
Member Author

eazyhozy commented Apr 1, 2026

@em3s Merged main and synced endpoint constants — added edges/cache/{cache}, removed POST /graph/v2/query (deleted in main). Force-pushed to fix incorrect committer on my 2 commits.

@eazyhozy eazyhozy force-pushed the feat/read-only-mode branch from 05303a5 to 935749c Compare April 1, 2026 01:11
@dosubot dosubot Bot added size:XXL This PR changes 1000+ lines, ignoring generated files. and removed size:XL This PR changes 500-999 lines, ignoring generated files. labels Apr 1, 2026
eazyhozy and others added 6 commits April 1, 2026 10:26
Introduce ReadOnlyRequestFilter that blocks mutating HTTP methods
(POST, PUT, DELETE, PATCH) on /graph/v2 and /graph/v3 paths when
actionbase.read-only=true. Read-only POST endpoints (/edges/get,
/multi-edges/ids, /query) are allowlisted.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Generated with [Claude Code](https://claude.ai/code)
via [Happy](https://happy.engineering)

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Happy <yesreply@happy.engineering>
- Add ReadOnlyModeDisabledStartUpTest (actionbase.read-only=false)
- Add default case test in StartUpTest (read-only not configured)

Generated with [Claude Code](https://claude.ai/code)
via [Happy](https://happy.engineering)

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Happy <yesreply@happy.engineering>
…urce

- Merge ReadOnlyModeStartUpTest and ReadOnlyModeDisabledStartUpTest into StartUpTest.kt
- Use @ObjectSourceParameterizedTest for read-only enabled/disabled cases
- Add default (not configured) case in StartUpTest
- Delete separate test files

Generated with [Claude Code](https://claude.ai/code)
via [Happy](https://happy.engineering)

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Happy <yesreply@happy.engineering>
Generated with [Claude Code](https://claude.ai/code)
via [Happy](https://happy.engineering)

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Happy <yesreply@happy.engineering>
…estFilterTest

Generated with [Claude Code](https://claude.ai/code)
via [Happy](https://happy.engineering)

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Happy <yesreply@happy.engineering>
eazyhozy and others added 7 commits April 1, 2026 10:26
…ReadOnlyRequestFilterTest

Constants serve as programmatic reference, @ObjectSource YAML serves as
independent spec-level assertions.

Generated with [Claude Code](https://claude.ai/code)
via [Happy](https://happy.engineering)

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Happy <yesreply@happy.engineering>
- protectedPaths -> paths
- mutatingMethods -> readMethods (inverted to allowlist)
- readOnlyPostSuffixes -> readSuffixes, isReadOnlyPost -> isRead
- Add strategy comment
- Early return for allowed cases

Generated with [Claude Code](https://claude.ai/code)
via [Happy](https://happy.engineering)

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Happy <yesreply@happy.engineering>
…e PATCH

- readMethods set -> readMethod = HttpMethod.GET
- Split allowed test into GET and read-only POST cases
- Add all graph query paths (scan, count, counts, agg)
- Remove PATCH (not used)

Generated with [Claude Code](https://claude.ai/code)
via [Happy](https://happy.engineering)

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Happy <yesreply@happy.engineering>
- GET: all v2/v3 query, admin, metadata, datastore endpoints
- Read-only POST: /query, /edges/get, /multi-edges/ids
- Blocked write: all v2/v3 POST/PUT/DELETE mutation endpoints
- Outside protected paths: PUT /graph/health/readiness
- readMethods -> readMethod (GET only)
- Remove PATCH (not used)
- Remove actuator paths from test (no write calls expected)

Generated with [Claude Code](https://claude.ai/code)
via [Happy](https://happy.engineering)

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Happy <yesreply@happy.engineering>
…ases

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…lter coverage

- Extract EndpointScanner to testFixtures for reusable controller annotation scanning
- Replace ObjectSource with MethodSource backed by reflection-scanned endpoints
- Declare all endpoints in READ/WRITE/NON_GRAPH constants with verbatim annotation paths
- Exhaustiveness test ensures scanned endpoints match declared constants
- Add Content-Type: application/json to read-only filter 403 response

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…ery)

Generated with [Claude Code](https://claude.ai/code)
via [Happy](https://happy.engineering)

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Happy <yesreply@happy.engineering>
@eazyhozy eazyhozy force-pushed the feat/read-only-mode branch from 935749c to 77bddcf Compare April 1, 2026 01:33
@dosubot dosubot Bot added size:XL This PR changes 500-999 lines, ignoring generated files. and removed size:XXL This PR changes 1000+ lines, ignoring generated files. labels Apr 1, 2026
@eazyhozy
Copy link
Copy Markdown
Member Author

eazyhozy commented Apr 1, 2026

@em3s Accidentally pushed a broken rebase that included main commits in the PR diff. To fix this, I reset to main and cherry-picked all PR commits preserving original author/committer. Force-pushed as a result. If you have a local checkout, please run:

git fetch origin && git reset --hard origin/feat/read-only-mode

@kakao kakao deleted a comment from CLAassistant Apr 1, 2026
- Replace explicit lambda parameter with `it` for cleaner syntax
@em3s em3s merged commit d936f56 into main Apr 1, 2026
7 checks passed
@em3s em3s added this to the v0.3.0 milestone Apr 21, 2026
em3s pushed a commit that referenced this pull request Apr 21, 2026
Backport read-only mode to `0.2.x`

- Cherry-pick #230, #239
- Register `POST /graph/v2/query` in `ReadOnlyRequestFilterTest` (0.2.x-only)

#236 excluded: 0.2.x has no `edges/cache → edges/seek` rename.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request size:XL This PR changes 500-999 lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add read-only mode to reject write operations

2 participants