Access Control v2 + detailed analytics + unified shell (2.1.0)#3
Merged
Conversation
…ck 1) Cookie-authenticated product writes now require admin, closing the gap where any JIT-provisioned modgud user could create/update/delete products. - ApiKeyFilter: cookie path requires AdminCheck.IsAdmin; non-admin cookie session → 403. Bearer API-key path (CI/CD machine track) unchanged. - SPA: /admin/products route gated with admin meta; Products sidebar link shown only to admins; Dashboard product affordances gated so non-admins don't dead-end into an admin-only surface. - Tests: split CookieAuth_WorksForProductWrites into CookieAuth_AdminCanWriteProducts (201) and CookieAuth_NonAdmin_CannotWriteProducts (403), exercising the real RBAC path. Concept: Atlas shelf/concept-access-control-v2 (enforcement points table). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…to-membership (Block 2a/2b) Additive, no behavior change yet (no enforcement; Restricted defaults false). Data model: - Group Marten document (permission carrier): MembershipMode, MemberEmails, MembershipScript, ReadProducts, IsAdminGroup, materialized AutoMemberUserIds, MembershipLastError, soft-delete. Registered in the shelf schema. - ProductConfig.Restricted flag, orthogonal to Visibility. - UserDocument claims snapshot (Claims map + flattened Permissions + ClaimsUpdatedAt). - IGroupService / MartenGroupService (soft-delete CRUD). Auto-membership (JsEval, copy+adapt from timetodo, in-memory — no event sourcing, no SQL translation): - Cocoar.JsEval.Engine 4.1.0; sandboxed predicate over a `user` global (user.email / user.permissions / user.claims), fail-closed on any error. - IGroupMembershipEvaluator (Evaluate → match + error) + JsEval impl. - IGroupMembershipRecalculator: per-user (login), per-group (save), all (manual); materializes AutoMemberUserIds. - ClaimsSnapshot extractor + reusable ModgudClaimsTransformation.ParsePermissions. - LoginAccessProcessor: refresh snapshot + recompute; wired into the real OIDC callback (OnTicketReceived) and the test sign-in seam. Full suite 110/110 green; format gates clean. Concept: Atlas shelf/concept-access-control-v2. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Enforcement (per-request AccessResolver, no JsEval in the hot path):
- IAccessResolver: memoized per-request grants (isAdmin incl. IsAdminGroup,
readable restricted products) from group membership (email ∪ materialized
auto ids) + token permission + allowlist.
- DocsRoutingMiddleware gates restricted products: authenticated-no-grant → 404
(no existence leak), anonymous HTML → /login redirect, anonymous asset → 401.
- Product API: GET /products hides restricted from non-members (admins see all,
badged); GET /products/{p} + /versions → 404 for non-members; Create/Update
accept Restricted; responses expose it.
- llms.txt index excludes restricted products.
- Admin policy, ApiKeyFilter cookie path and /auth/me now route through the
resolver, so IsAdminGroup members are admins everywhere.
Groups API (admin-gated): CRUD, manual recalc, JsEval dry-run (test-script).
Tests: 13 AccessControlTests cover middleware gating (404/redirect/401), API
filtering, group adminship, and JsEval auto-membership end-to-end
(create auto group → matching user login → materialized grant → served).
Full suite 123/123; format gates clean.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…d toggle (Block 2d)
- Groups page (admin-only, top-level nav): grid + routed tabbed modal
(General / Members / Auto-membership / Products), mirroring the products
view conventions (grid store, rowDataRef, routed fragment modal).
- General: name, description, membership mode (Manual/Auto), IsAdminGroup.
- Members: explicit email tag-chips + read-only resolved auto-members.
- Auto-membership: monospace JS predicate editor, last-error note, and a
"test against a user" dry-run (POST /groups/test-script).
- Products: read-grant multi-select from the products list.
- Toolbar "Recalculate" triggers manual membership recompute.
- Product modal: Restricted toggle (General tab) with an explanatory note
linking to Groups; wired through create/update; grid gains a Restricted column.
- Users page: memberships column (groups a user belongs to) + IsAdminViaGroup,
computed server-side in the users list endpoint.
- groups store + shelf-api methods + Group/Restricted TS models.
- Router: /admin/groups (admin meta + routed modal); sidebar Groups link.
Client type-checks + builds; backend suite 123/123; format gates clean.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Replaces the plain textarea with the Monaco-based CoarScriptEditor from the separate @cocoar/vue-script-editor package (2.17.1 — not part of vue-ui): TypeScript language mode, scriptMode (no body diagnostics), and a hidden preamble declaring `user` so the editor gives IntelliSense on user.email / user.permissions / user.claims. CSS wired via @import "@cocoar/vue-script-editor/styles". Verified locally against the dev stack (modgud OIDC login) with chrome-devtools: groups page + nav gating, create modal + tab/mode switching, editor renders, group create → grid, JsEval dry-run against the live backend (match true/false), product Restricted toggle + grid column. Fixed an editor-height collapse (height needs a unit: "180px", not "180"). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ncipal) Flips the read-grant direction to the product side, per review: instead of a group listing its products, a restricted product lists the principals that may read it. Both Group and UserDocument now implement IPrincipal, so either can be assigned — matching timetodo's Principal model. - IPrincipal (Group + UserDocument) + PrincipalKind + PrincipalRef value type (group by id, user by email — email lets a grant be staged before first login). - ProductConfig.ReadPrincipals replaces Group.ReadProducts. - AccessResolver: grants now carry (isAdmin, groupIds, email); CanRead(product) tests the product's ReadPrincipals against them — no group ReadProducts scan. - GET /_api/principals: assignable groups + users for the product access picker. - Product create/update accept + expose ReadPrincipals; group upsert drops ReadProducts. Enum JSON as strings (PrincipalKind → "Group"/"User"). - Tests: grants moved product-side; added RestrictedDocs_DirectUserGrant_IsServed. Full suite 124/124; format gates clean. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…istration - Product modal Access section: when Restricted, a "Who may read" picker assigns principals — a searchable groups+users dropdown (from GET /principals) plus a free-email input to stage a user before their first login. Chips show kind icon. - Group modal loses the Products tab; group grid loses the Read-grants column (grants now live on the product). - Groups page moves under Administration (like Users): /admin/settings/groups sub-nav entry; top-level Groups sidebar link removed; header owned by the settings layout. - shelf-api getPrincipals; Product.readPrincipals + PrincipalRef/Principal models. Verified locally with chrome-devtools against the dev stack: Groups under Administration, picker lists groups + users, assigning a group and a staged user by email round-trips (product readPrincipals persists both kinds). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ct modal The Restricted toggle + "Who may read" principal picker move out of the General tab into a dedicated Access tab (General / Access / Tags & API / Versions), giving access control its own space instead of a nested block at the bottom of General. Behavior unchanged. Verified in the browser. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ode everywhere)
The public landing and the admin UI were two separate shells with different
headers; the landing also had no dark mode. Now both share one AppLayout:
- AppLayout (generalized from AdminLayout): the branded header is always present
(logo, title via useUI, dark/light toggle); the right side shows the avatar
menu when authenticated or a Sign-in button when anonymous. The admin sidebar
renders only inside /admin (the landing has none).
- Router: landing (/) and the admin routes are children of AppLayout, so the
whole SPA is one consistent view. AdminLayout removed.
- LandingView: drops its own custom header (moved to the shell), sets its title
via useUI, and its CSS is migrated to vue-ui theme tokens so dark mode works on
the docs index too.
Note: this is the SPA (landing + admin). The VitePress-rendered product docs
(/{product}/{version}/…) are separate static content with their own theme.
Verified in the browser: shared header on landing + admin, sidebar only in
admin, Sign-in button when anonymous, dark mode on the landing.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ding) The sidebar now renders whenever the user is a logged-in admin — on every page including the public landing — instead of only inside /admin. Anonymous visitors and non-admin users get no sidebar. The header logo reserves the sidebar width accordingly. Verified in the browser (admin landing shows the sidebar). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…breakdowns, IP filter) All analysis stays in the backend; the browser only renders. GetSummary now computes, in one pass over the filtered entries: - countriesCount / citiesCount, uniqueIps-per-day - topCities (city + country code), topCountries (top 30) - locations: geolocated points (lat/lng + city/country + count) for the map - byBrowser / byOs (User-Agent classified server-side), byLanguage (Accept-Language primary tag), topReferrers (referer host) - excludeIps filter (also on /visits) to hide specific IPs from every aggregate GeoIpService.Lookup now also returns lat/lng; AccessLogEntry gains Latitude/Longitude (Marten JSONB — no migration), set during log persistence. Country stays the 2-letter ISO code (name + flag are derived client-side). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Rebuilds AnalyticsView into a full dashboard rendering the backend aggregates: - Filter bar: date range + product selector + IP-exclude input (all server-side). - KPI tiles: visits, unique visitors, countries, cities, top product. - World map (@cocoar/vue-map / Leaflet): one bubble per city, colored by volume bucket (low/mid/high) with a legend, plus a Top-Cities sidebar. - Visits-by-day chart (SVG) with unique-visitor tooltip. - Top Countries + Cities with flag emoji + full name (derived client-side from the ISO code via Intl.DisplayNames — display only, not analysis). - Top Products/Pages and Browser/OS/Language/Referrer breakdowns. - Access log: country column shows flag + full name. Verified in the browser against seeded geo data: map bubbles, flags, all breakdowns, and the IP-exclude filter (49 → 41 when excluding a city's IP). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…dows) Windows doesn't render regional-indicator flag emoji (it shows the bare letters), so the flags were invisible there. Switch to flag-icons (SVG): - geo.ts exposes flagClass(code) → "fi fi-at" instead of an emoji. - TopList renders an optional flag prefix; Top Countries/Cities pass the code. - Access log country column uses a cell renderer with the flag span. - Import flag-icons CSS from main.ts (not a Tailwind @import) so Vite's asset pipeline rewrites the flag url()s to bundled paths — otherwise they resolved to a non-existent /flags/*.svg and 404'd. Verified in the browser: real AT/US/GB/DE/JP flags now render in the lists. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…set) The "users-round" icon isn't in the app's icon set, so the Groups sub-nav item, the group modal, and the product group-principal chip showed no glyph. Per the timetodo convention (users / shield for permission entities), Groups now uses "shield" (distinct from the Users item's "users"), the group modal matches, and the product access chip uses "users" (group) vs "user" (single user). Verified in the browser: the Groups sub-nav icon renders. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…P login docs) - CHANGELOG: 2.1.0 — restricted products, permission groups, product-side principal grants, detailed analytics dashboard, unified shell, and the admin-only product-write security fix. - CLAUDE.md: access-control-v2 architecture, API surface (groups/principals, product writes require admin, /login server route), key files, design decisions. - website: admin-ui.md gains the Access tab, an Access Control section (restricted products + groups + auto-membership) and the new Groups admin section; the Analytics section describes the map/flags/breakdowns/filters. authentication.md is corrected from the old email-OTP flow to the OIDC code flow, notes admin via group, that product writes require admin, and drops the removed /auth/otp endpoints. VitePress build is clean. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Restricted documentation with permission groups, a much richer analytics dashboard, and a unified app shell. Backwards-compatible — existing products stay public; nothing changes until a product is marked restricted. Targets release 2.1.0.
Highlights
Access Control v2
ProductConfig.Restricted— a restricted product is hidden from non-grantees and returns 404 on unauthorized access (no existence leak); anonymous → login redirect, admins see all. Enforced inDocsRoutingMiddleware; products API / landing /llms.txtfilter it out.Group): explicit email members (stageable pre-login) and/or JsEval auto-membership over the persisted login claims snapshot (Cocoar.JsEval.Engine, in-memory copy+adapt from timetodo, fail-closed).IsAdminGroupgrants adminship.GroupandUserDocumentare bothIPrincipal; a product'sReadPrincipalsassigns groups and individual users (by email).IAccessResolver(per-request, memoized) — no JsEval in the hot path./_api/groups/*+/_api/principals.Detailed analytics
@cocoar/vue-map), country/city breakdowns with SVG flags (flag-icons— emoji flags don't render on Windows) + full names, browser/OS/language/referrer breakdowns, per-product + IP-exclude filters. All aggregation server-side; access log stores lat/lng.Unified shell
AppLayout: same header everywhere (dark/light toggle), admin sidebar for logged-in admins on every page, dark mode on the landing.Docs
admin-ui.md+authentication.md(also corrects stale 2.0 email-OTP docs → OIDC code flow).Verification
AccessControlTestscovering gating, filtering, group adminship, direct-user grants, JsEval auto-membership end-to-end).Concept: Atlas
shelf/concept-access-control-v2.🤖 Generated with Claude Code