Skip to content

COG-1154: W1 · UserResource enable/disable → POST (server + Angular caller) - #338

Open
patrickbradley-cog wants to merge 1 commit into
masterfrom
devin/cog-1154-w1-userresource-enabledisable-post-server-angular-caller
Open

patrickbradley-cog wants to merge 1 commit into
masterfrom
devin/cog-1154-w1-userresource-enabledisable-post-server-angular-caller

Conversation

@patrickbradley-cog

@patrickbradley-cog patrickbradley-cog commented Sep 30, 2026 •

Copy link
Copy Markdown

Linear: COG-1154 — W1 forward-compatible refactor on Boot 1.5.4 (no Boot/Java bump).

Summary

/api/user/{username}/enable and /disable were mapped with a bare @RequestMapping (no method), so they accepted GET. Once W2 turns CSRF on, CsrfFilter skips safe methods, so those GETs would stay forgeable (<img src=".../api/user/bob/disable">). This converts them to POST now, while CSRF is still off, so the change is behaviour-neutral.

- @RequestMapping("/user/{username}/enable")    // any method
+ @PostMapping("/user/{username}/enable")       // GET -> 405
  (same for /disable)

AdminPortal UserService.enableUser/disableUser:

this.http.post(url, null, { headers: this.xsrfHeaders(), withCredentials: true })
// xsrfHeaders(): adds X-XSRF-TOKEN = XSRF-TOKEN cookie value, only if that cookie exists

The header is only added when the XSRF-TOKEN cookie is present. Today (CSRF disabled) there is no such cookie, so the request stays a CORS "simple" request (POST, no body/content-type, no custom header) and needs no preflight changes.

Review notes

Verification

mvn -B -f UserFront/pom.xml verify (Temurin 1.8.0_504, MySQL 5.7 container, SPRING_DATASOURCE_URL=...?useSSL=false because Temurin 8 disables TLSv1 used by connector 5.1.42):

Tests run: 4, Failures: 0, Errors: 0, Skipped: 0 - in com.userFront.resource.UserResourceTest
Tests run: 1, Failures: 0, Errors: 0, Skipped: 0 - in com.userFront.UserFrontApplicationTests
Tests run: 5, Failures: 0, Errors: 0, Skipped: 0
BUILD SUCCESS

AdminPortal (Node 12): ng build succeeds. ng lint reports no new issues on the changed lines (the remaining errors in user.service.ts are on unchanged lines and were already there).

Manual run: app jar started against MySQL; signed up admin1 + alice, gave admin1 ROLE_ADMIN, logged in via POST /index:

$ curl -b cj.txt -X GET  /api/user/alice/disable   -> HTTP 405 "Request method 'GET' not supported"   DB: alice enabled=1
$ curl -b cj.txt -X GET  /api/user/alice/enable    -> HTTP 405                                       DB: alice enabled=1
$ curl -b cj.txt -X POST /api/user/alice/disable   -> HTTP 200                                       DB: alice enabled=0
$ curl -b cj.txt -X POST /api/user/alice/enable    -> HTTP 200                                       DB: alice enabled=1
$ curl -b cj.txt -X POST -H "Origin: http://localhost:4200" /api/user/alice/disable
  HTTP/1.1 200, Access-Control-Allow-Origin: http://localhost:4200, Access-Control-Allow-Credentials: true

Link to Devin session: https://app.devin.ai/sessions/604694d8ca1744fa8f2aef2722fcf9d0
Open in Devin Desktop: https://app.devin.ai/desktop/session/604694d8ca1744fa8f2aef2722fcf9d0?variant=devin
Requested by: @patrickbradley-cog


Devin Review

Status Commit
⚪ Not started —

Run Devin Review

Devin Review (Staging)


Devin Review

Co-Authored-By: patrick.bradley <patrick.bradley@cognition.ai>
@devin-ai-integration

Copy link
Copy Markdown

I'll fix CI failures and address comments from users with write access. I'll skip comments containing "(aside)".

  • Disable automatic comment, CI, and merge conflict monitoring

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Devin Review found 2 potential issues.

Devin Review

const headers = new Headers();
const match = document.cookie.match(/(?:^|;\s*)XSRF-TOKEN=([^;]*)/);
if (match) {
headers.append('X-XSRF-TOKEN', decodeURIComponent(match[1]));

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔴 CSRF cookie blocks admin account actions

When xsrfHeaders finds an XSRF cookie, both account actions fail from the Angular portal. The custom header triggers a preflight, but RequestFilter excludes x-xsrf-token from allowed headers.

Learn more

The admin portal runs on localhost:4200 and sends credentialed requests to localhost:8080. An XSRF-TOKEN cookie makes xsrfHeaders add a custom header to each POST. Browsers then send an OPTIONS preflight before the POST. RequestFilter responds with an allow-list that omits x-xsrf-token, so the browser never sends the action. The new standalone MVC tests do not exercise browser CORS behavior.

Example: An admin loads the portal after localhost:8080 issues an XSRF-TOKEN cookie. Clicking Disable sends an OPTIONS request asking to use x-xsrf-token; the response rejects the header, so the user remains enabled.

Recommended fix: Add x-xsrf-token to RequestFilter's preflight Access-Control-Allow-Headers list in the same change as the new header, and add a test for an OPTIONS request with Access-Control-Request-Headers: x-xsrf-token.

Devin Review


Was this helpful? React with 👍 or 👎 to provide feedback.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Intentionally not changed here. XSRF-TOKEN is only issued once W2 turns CSRF on (CookieCsrfTokenRepository). CSRF is disabled on this branch, so xsrfHeaders() returns empty headers and the POST stays a CORS simple request with no preflight. I checked this live: POST from Origin http://localhost:4200 returns 200 with the ACAO/ACAC headers (evidence in the PR body). The allow-list fix is owned by W2 sibling COG-1165, which replaces RequestFilter with a CorsConfigurationSource whose allowedHeaders already includes x-xsrf-token. Editing RequestFilter here would break the file-disjoint slicing and conflict with that ticket.


@Before
public void setUp() {
mockMvc = MockMvcBuilders.standaloneSetup(userResource).build();

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔍 POST tests omit security configuration

standaloneSetup tests routing without Spring Security. An integration test can verify that only admins can use the new POST endpoints.

Devin Review


Was this helpful? React with 👍 or 👎 to provide feedback.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This is on purpose. UserResourceTest only checks the HTTP-method mapping, which is the change in this PR. Authorization (@PreAuthorize("hasRole('ADMIN')") on the class) is untouched. Real-app coverage with security on comes from the W0 Testcontainers admin smoke suite, and this PR merges after it. I also exercised the endpoints manually against the running app as a logged-in ROLE_ADMIN user (see the PR body).

@devin-ai-integration

Copy link
Copy Markdown

Browser verification (AdminPortal :4200 → UserFront :8080 → MySQL 5.7)

Caveat: normal clicks persisted correctly, but the existing location.reload() immediately after subscribe() in user-account.component.ts makes DevTools mark those XHRs as canceled. This code predates the PR. The POST 200 Network screenshots come from a separate run where a debugger breakpoint held the reload. No source was changed for that run.

  • Passed: Disable changes alice to false and Enable changes her back to true, both on the automatic reload. MySQL agrees each time.
  • Passed: an authenticated browser GET to /api/user/alice/enable returns 405 Request method 'GET' not supported, and alice stays disabled in MySQL.
  • Passed (reload held): both POSTs return 200 with Access-Control-Allow-Origin: http://localhost:4200 and Access-Control-Allow-Credentials: true, with no CORS error.
  • Untested: the branch where the XSRF-TOKEN cookie is present. CSRF is off until W2, so no cookie is issued yet.
Before Disable: alice=true After Disable: alice=false
Before Disable After Disable
After Enable: alice=true GET enable → 405
After Enable GET enable 405
Network: POST 200, with a debugger breakpoint holding the reload
Disable POST 200 Enable POST 200
Disable POST 200 Enable POST 200
MySQL state (select username, enabled+0 from user)
                          admin1  alice
Baseline                     1      1
After Disable                1      0
After browser GET enable     1      0
After Enable                 1      1
Recordings

Natural click flow:

natural flow

Debugger-delayed reload (Network evidence):

paused network flow

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