Rewrite CLAUDE.md for IPS project, remove Barycenter content

Replace the incorrectly copied Barycenter (OIDC IdP) content with
accurate IPS project documentation including workspace structure,
build commands, error handling conventions, architecture overview,
and a pkg5 legacy compatibility reference mapping our docs and
implementation to the original pkg5 specifications.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Till Wegmueller 2026-02-25 20:05:26 +01:00
parent 7d5ddb626f
commit 745d610a0a
No known key found for this signature in database

585
CLAUDE.md
View file

@ -4,492 +4,111 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
## Project Overview ## Project Overview
Barycenter is an OpenID Connect Identity Provider (IdP) implementing OAuth 2.0 Authorization Code flow with PKCE. The project is written in Rust using axum for the web framework, SeaORM for database access (SQLite and PostgreSQL), and josekit for JOSE/JWT operations. IPS (Image Packaging System) is a Rust reimplementation of the illumos/OpenIndiana package management system (replacing the Python-based pkg5). It handles software packaging, installation, updates, and dependency resolution for illumos distributions.
**Workspace crates:** libips (core library), pkg6 (client CLI), pkg6repo (repo management CLI), pkg6depotd (HTTP depot server), pkg6recv, pkgtree, specfile, ports, userland, xtask.
## Build and Development Commands ## Build and Development Commands
```bash ```bash
# Build the project cargo build # Build all crates
cargo build cargo check # Check without building
cargo nextest run # Run tests (CRITICAL: use nextest, not cargo test)
# Run the application (defaults to config.toml) cargo nextest run test_name # Run specific test
cargo run make release # Build release + copy to artifacts/
RUST_LOG=libips=trace cargo run # Run with logging
# Run with custom config
cargo run -- --config path/to/config.toml
# Run in release mode
cargo build --release
cargo run --release
# Check code without building
cargo check
# Run tests (IMPORTANT: use cargo nextest, not cargo test)
cargo nextest run
# Run with logging (uses RUST_LOG environment variable)
RUST_LOG=debug cargo run
RUST_LOG=barycenter=trace cargo run
``` ```
## Testing **CRITICAL: Always use `cargo nextest run` instead of `cargo test`.** Tests need process isolation to avoid port conflicts.
**CRITICAL: Always use `cargo nextest run` instead of `cargo test`.** ## Coding Conventions
This project uses [cargo-nextest](https://nexte.st/) for running tests because: ### Error Handling (miette + thiserror)
- Tests run in separate processes, preventing port conflicts in integration tests
- Better test isolation and reliability - All error enums derive `#[derive(Debug, Error, Diagnostic)]`
- Cleaner output and better performance - Each variant has `#[diagnostic(code(...), help(...))]` with actionable guidance
- Subsystem errors chain via `#[error(transparent)] #[diagnostic(transparent)]`
Install nextest if you don't have it: - Return `IpsResult<T>` (alias for `Result<T, IpsError>`) from the top-level API
```bash - Subsystem functions return their own result type (e.g., `RepositoryResult<T>`, `ImageResult<T>`, `FmriResult<T>`)
cargo install cargo-nextest - Each module defines: `pub type Result<T> = std::result::Result<T, ModuleError>;`
``` - Diagnostic codes follow the pattern `ips::module::variant` (e.g., `ips::image_error::io`)
Run tests: ### Rust Style
```bash
# Run all tests - Use strongly typed idiomatic Rust — express logic in datatypes
cargo nextest run - Do error handling — express options for how to handle issues for calling functions
- Use miette's diagnostic pattern to inform the user thoroughly what they need to do
# Run with verbose output - Never use raw SQL — use rusqlite with proper abstractions
cargo nextest run --verbose - Edition 2024
# Run specific test ## Architecture
cargo nextest run test_name
``` ### libips (core library)
- `fmri.rs` — FMRI parsing (`pkg://publisher/name@version`)
## Configuration - `actions/` — Manifest action types (File, Dir, Link, User, Group, etc.) and filesystem executors
- `image/` — Image metadata, catalog queries, installed packages DB (SQLite)
The application loads configuration from: - `repository/``ReadableRepository`/`WritableRepository` traits, FileBackend (local), RestBackend (HTTP)
1. Default values (defined in `src/settings.rs`) - `solver/` — Dependency resolution via resolvo
2. Configuration file (default: `config.toml`) - `digest/` — SHA1/SHA2/SHA3 hashing
3. Environment variables with prefix `BARYCENTER__` (e.g., `BARYCENTER__SERVER__PORT=9090`) - `payload/` — Package payload handling
- `publisher.rs` — Publisher configuration
Environment variables use double underscores as separators for nested keys. - `transformer.rs` — Manifest transformation/linting
- `depend/` — Dependency generation and parsing
### Database Configuration
### Key Types
Barycenter supports both SQLite and PostgreSQL databases. The database backend is automatically detected from the connection string: - `Fmri` — Fault Management Resource Identifier (`pkg://publisher/name@release,branch-build:timestamp`)
- `Manifest` — Collection of actions describing a package
**SQLite (default):** - `Image` — Full or Partial image with metadata, publishers, catalogs
```toml - `FileBackend` / `RestBackend` — Repository implementations
[database]
url = "sqlite://barycenter.db?mode=rwc" ## pkg5 Legacy Documentation and Compatibility
```
We carry the original pkg5 reference documentation in `doc/pkg5_docs/` to guide compatibility with the legacy Python client. Our own docs in `doc/` describe how we've implemented the compatibility layer.
**PostgreSQL:**
```toml ### Reference Specifications (from pkg5)
[database]
url = "postgresql://user:password@localhost/barycenter" | Topic | File | What it covers |
``` |-------|------|----------------|
| **Depot protocol** | `depot.txt`, `depot.rst` | HTTP REST API: `/file/0/{hash}`, `/catalog/0/`, `/manifest/0/{fmri}`, `/search/0/`, `/p5i/0/`, `/publisher/0/`, `/versions/0/` |
Or via environment variable: | **Retrieval protocol** | `guide-retrieval-protocol.rst` | Client-depot wire protocol, ETag/If-Modified-Since caching, content negotiation |
```bash | **Publication protocol** | `guide-publication-protocol.rst` | Transaction-based publishing (`open`, `add`, `close`) |
export BARYCENTER__DATABASE__URL="postgresql://user:password@localhost/barycenter" | **Repository format** | `guide-repository-format.rst` | On-disk layout: `pkg/`, `file/` (two-level hash dirs), `catalog/`, `trans/`, `updatelog/` |
``` | **Catalog v1** | `catalog-v1.txt`, `catalog.txt` | Delta-encoded text catalog with `catalog.attrs`, `npkgs`, incremental updates |
| **Actions** | `actions.txt`, `actions.rst` | All 12 action types: file, dir, link, hardlink, depend, service, user, group, driver, license, legacy, set |
## Architecture and Module Structure | **FMRI & versioning** | `versions.txt`, dev-guide `chpt3.txt` | `pkg://publisher/name@release,branch-build:timestamp`, component ordering |
| **Image types** | `image.txt`, `image.rst` | Full, Zone, User images and their metadata layout |
### Entry Point (`src/main.rs`) | **Server API versions** | `server_api_versions.txt`, `client_api_versions.txt` | Protocol version negotiation between client and depot |
The application initializes in this order: | **Signed manifests** | `signed_manifests.txt` | Manifest signature format and verification |
1. Parse CLI arguments for config file path | **Linked images** | `linked-images.txt`, `parallel-linked-images.txt` | Zone/parent-child image constraints |
2. Load settings from config file and environment | **Facets & variants** | `facets.txt` | Conditional action delivery (`variant.arch`, `facet.doc`) |
3. Initialize database connection and create tables via `storage::init()` | **Mediated links** | `mediated-links.txt` | Conflict resolution for symlinks across packages |
4. Initialize JWKS manager (generates or loads RSA keys) | **On-disk format** | `on-disk-format.txt` | Container format proposal for packages |
5. Start web server with `web::serve()` | **Dev guide** | `dev-guide/chpt1-14.txt` | Full IPS developer guide chapters |
### Settings (`src/settings.rs`) ### Our Compatibility Implementation
Manages configuration with four main sections:
- `Server`: listen address and public base URL (issuer) | Feature | Our doc | Implementation |
- `Database`: database connection string (SQLite or PostgreSQL) |---------|---------|----------------|
- `Keys`: JWKS and private key paths, signing algorithm | **p5i publisher files** | `doc/pub_p5i_implementation.md` | `FileBackend` generates `pub.p5i` JSON files per publisher for backward compat with `pkg set-publisher -p` |
- `Federation`: trust anchor URLs (future use) | **Obsoleted packages** | `doc/obsoleted_packages.md` | Detects `pkg.obsolete=true` during pkg5 import; stores in `<repo>/obsoleted/` with structured metadata |
| **pkg5 import** | `pkg6repo/src/pkg5_import.rs` | Reads `pkg5.repository`, two-level hash dirs, gzip payloads, URL-encoded versions; converts to pkg6 format |
The `issuer()` method returns the OAuth issuer URL, preferring `public_base_url` or falling back to `http://{host}:{port}`. | **Depot REST API** | `pkg6depotd/src/http/` | Serves same HTTP paths as pkg5 depot (`/file/0/`, `/manifest/0/`, `/catalog/0/`, `/publisher/0/`) |
| **libips integration** | `doc/forge_docs/ips_integration.md` | Typed API replacing pkg5 CLI tools (`pkgsend`, `pkgmogrify`, `pkgdepend`, `pkglint`, `pkgrepo`) |
### Storage (`src/storage.rs`) | **Error handling** | `doc/rust_docs/error_handling.md` | miette + thiserror patterns with `ips::module::variant` codes |
Database layer with raw SQL using SeaORM's `DatabaseConnection`. Supports both SQLite and PostgreSQL backends, automatically detected from the connection string. Tables:
- `clients`: OAuth client registrations (client_id, client_secret, redirect_uris) ### Key Compatibility Surfaces for Legacy Client
- `auth_codes`: Authorization codes with PKCE challenge, subject, scope, nonce
- `access_tokens`: Bearer tokens with subject, scope, expiration The legacy `pkg(1)` Python client expects these from a depot server:
- `properties`: Key-value store for arbitrary user properties (owner, key, value) 1. **`/versions/0/`** — lists supported operations and protocol versions
2. **`/publisher/0/`** — returns `application/vnd.pkg5.info` (p5i JSON)
All IDs and tokens are generated via `random_id()` (24 random bytes, base64url-encoded). 3. **`/catalog/0/`** — catalog with `Last-Modified` header, incremental updates
4. **`/manifest/0/{fmri}`** — `text/plain` manifest with `ETag`
### JWKS Manager (`src/jwks.rs`) 5. **`/file/0/{hash}`** — gzip-compressed file content with `ETag` and `Cache-Control`
Handles RSA key generation, persistence, and JWT signing: 6. **`/search/0/{token}`** — search returning `index action value package` tuples
- Generates 2048-bit RSA key on first run
- Persists private key as JSON to `private_key_path` ## Documentation Maintenance
- Publishes public key set to `jwks_path`
- Provides `sign_jwt_rs256()` for ID Token signing with kid header - Keep `docs/ai/architecture.md` updated when making structural changes. Bump the "last updated" date.
- Create a new timestamped plan in `docs/ai/plans/` before starting a new phase or significant feature.
### Web Endpoints (`src/web.rs`) - Create a new timestamped ADR in `docs/ai/decisions/` when making meaningful technology or design choices. Number sequentially from the last ADR.
Implements OpenID Connect and OAuth 2.0 endpoints: - Never delete old plans or decisions. Mark superseded plans with status `Superseded` and link to the replacement.
**Discovery & Registration:**
- `GET /.well-known/openid-configuration` - OpenID Provider metadata
- `GET /.well-known/jwks.json` - Public signing keys
- `POST /connect/register` - Dynamic client registration
**OAuth/OIDC Flow:**
- `GET /authorize` - Authorization endpoint (issues authorization code with PKCE)
- Validates client_id, redirect_uri, scope (must include "openid"), PKCE S256
- Checks 2FA requirements (admin-enforced, high-value scopes, max_age)
- Redirects to /login or /login/2fa if authentication needed
- Returns redirect with code and state
- `POST /token` - Token endpoint (exchanges code for tokens)
- Supports `client_secret_basic` (Authorization header) and `client_secret_post` (form body)
- Validates PKCE S256 code_verifier
- Returns access_token, id_token (JWT with AMR/ACR claims), token_type, expires_in
- `GET /userinfo` - UserInfo endpoint (returns claims for Bearer token)
**Authentication:**
- `GET /login` - Login page with passkey autofill and password fallback
- `POST /login` - Password authentication, checks 2FA requirements
- `GET /login/2fa` - Two-factor authentication page
- `POST /logout` - End user session
**Passkey/WebAuthn Endpoints:**
- `POST /webauthn/register/start` - Start passkey registration (requires session)
- `POST /webauthn/register/finish` - Complete passkey registration
- `POST /webauthn/authenticate/start` - Start passkey authentication (public)
- `POST /webauthn/authenticate/finish` - Complete passkey authentication
- `POST /webauthn/2fa/start` - Start 2FA passkey verification (requires partial session)
- `POST /webauthn/2fa/finish` - Complete 2FA passkey verification
**Passkey Management:**
- `GET /account/passkeys` - List user's registered passkeys
- `DELETE /account/passkeys/:credential_id` - Delete a passkey
- `PATCH /account/passkeys/:credential_id` - Update passkey name
**Non-Standard:**
- `GET /properties/:owner/:key` - Get property value
- `PUT /properties/:owner/:key` - Set property value
- `GET /federation/trust-anchors` - List trust anchors
### Error Handling (`src/errors.rs`)
Defines `CrabError` for internal error handling with conversions from common error types.
## Key Implementation Details
### PKCE Flow
- Only S256 code challenge method is supported (plain is rejected)
- Code challenge stored with auth code
- Code verifier validated at token endpoint by hashing and comparing
### Client Authentication
Token endpoint accepts two methods:
1. `client_secret_basic`: HTTP Basic auth (client_id:client_secret base64-encoded)
2. `client_secret_post`: Form parameters (client_id and client_secret in body)
### ID Token Claims
Generated ID tokens include:
- Standard claims: iss, sub, aud, exp, iat
- Optional: nonce (if provided in authorize request)
- at_hash: hash of access token per OIDC spec (left 128 bits of SHA-256, base64url)
- auth_time: timestamp of authentication (from session)
- amr: Authentication Method References array (e.g., ["pwd"], ["hwk"], ["pwd", "hwk"])
- acr: Authentication Context Reference ("aal1" for single-factor, "aal2" for two-factor)
- Signed with RS256, includes kid header matching JWKS
### State Management
- Authorization codes: 5 minute TTL, single-use (marked consumed)
- Access tokens: 1 hour TTL, checked for expiration and revoked flag
- Sessions: Track authentication methods (AMR), context (ACR), and MFA status
- WebAuthn challenges: 5 minute TTL, cleaned up every 5 minutes by background job
- All stored in database with timestamps
### WebAuthn/Passkey Authentication
Barycenter supports passwordless authentication using WebAuthn/FIDO2 passkeys with the following features:
**Authentication Modes:**
- **Single-factor passkey login**: Passkeys as primary authentication method
- **Two-factor authentication**: Passkeys as second factor after password login
- **Password fallback**: Traditional password authentication remains available
**Client Implementation:**
- Rust WASM module (`client-wasm/`) compiled with wasm-pack
- Browser-side WebAuthn API calls via wasm-bindgen
- Conditional UI support for autofill in Chrome 108+, Safari 16+
- Progressive enhancement: falls back to explicit button if autofill unavailable
**Passkey Storage:**
- Full `Passkey` object stored as JSON in database
- Tracks signature counter for clone detection
- Records backup state (cloud-synced vs hardware-bound)
- Supports friendly names for user management
**AMR (Authentication Method References) Values:**
- `"pwd"`: Password authentication
- `"hwk"`: Hardware-bound passkey (YubiKey, security key)
- `"swk"`: Software/cloud-synced passkey (iCloud Keychain, password manager)
- Multiple values indicate multi-factor auth (e.g., `["pwd", "hwk"]`)
**2FA Enforcement Modes:**
1. **User-Optional 2FA**: Users can enable 2FA in account settings (future UI)
2. **Admin-Enforced 2FA**: Set `users.requires_2fa = 1` via GraphQL mutation
3. **Context-Based 2FA**: Triggered by:
- High-value scopes: "admin", "payment", "transfer", "delete"
- Fresh authentication required: `max_age < 300` seconds
- Can be configured per-scope or per-request
**2FA Flow:**
1. User logs in with password → creates partial session (`mfa_verified=0`)
2. If 2FA required, redirect to `/login/2fa`
3. User verifies with passkey
4. Session upgraded: `mfa_verified=1`, `acr="aal2"`, `amr=["pwd", "hwk"]`
5. Authorization proceeds, ID token includes full authentication context
## Current Implementation Status
See `docs/oidc-conformance.md` for detailed OIDC compliance requirements.
**Implemented:**
- Authorization Code flow with PKCE (S256)
- Dynamic client registration
- Token endpoint with client_secret_basic and client_secret_post
- ID Token signing (RS256) with at_hash, nonce, auth_time, AMR, and ACR claims
- UserInfo endpoint with Bearer token authentication
- Discovery and JWKS publication
- Property storage API
- User authentication with sessions
- Password authentication with argon2 hashing
- WebAuthn/passkey authentication (single-factor and two-factor)
- WASM client for browser-side WebAuthn operations
- Conditional UI/autofill for passkey login
- Three 2FA modes: user-optional, admin-enforced, context-based
- Background jobs for cleanup (sessions, tokens, challenges)
- Admin GraphQL API for user management and job triggering
- Refresh token grant with rotation
- Session-based AMR/ACR tracking
**Pending:**
- Cache-Control headers on token endpoint
- Consent flow (currently auto-consents)
- Token revocation and introspection endpoints
- OpenID Federation trust chain validation
- User account management UI
## Admin GraphQL API
The admin API is served on a separate port (default: 9091) and provides GraphQL queries and mutations for management:
**Mutations:**
```graphql
mutation {
# Trigger background jobs manually
triggerJob(jobName: "cleanup_expired_sessions") {
success
message
}
# Enable 2FA requirement for a user
setUser2faRequired(username: "alice", required: true) {
success
message
requires2fa
}
}
```
**Queries:**
```graphql
query {
# Get job execution history
jobLogs(limit: 10, onlyFailures: false) {
id
jobName
startedAt
completedAt
success
recordsProcessed
}
# Get user 2FA status
user2faStatus(username: "alice") {
username
requires2fa
passkeyEnrolled
passkeyCount
passkeyEnrolledAt
}
# List available jobs
availableJobs {
name
description
schedule
}
}
```
Available job names:
- `cleanup_expired_sessions` (hourly at :00)
- `cleanup_expired_refresh_tokens` (hourly at :30)
- `cleanup_expired_challenges` (every 5 minutes)
## Building the WASM Client
The passkey authentication client is written in Rust and compiled to WebAssembly:
```bash
# Install wasm-pack if not already installed
cargo install wasm-pack
# Build the WASM module
cd client-wasm
wasm-pack build --target web --out-dir ../static/wasm
# The built files will be in static/wasm/:
# - barycenter_webauthn_client_bg.wasm
# - barycenter_webauthn_client.js
# - TypeScript definitions (.d.ts files)
```
The WASM module is automatically loaded by the login page and provides:
- `supports_webauthn()`: Check if WebAuthn is available
- `supports_conditional_ui()`: Check for autofill support
- `register_passkey(options)`: Create a new passkey
- `authenticate_passkey(options, mediation)`: Authenticate with passkey
## Testing and Validation
### Manual Testing Flow
**1. Test Password Login:**
```bash
# Navigate to http://localhost:9090/login
# Enter username: admin, password: password123
# Should create session and redirect
```
**2. Test Passkey Registration:**
```bash
# After logging in with password
# Navigate to http://localhost:9090/account/passkeys
# (Future UI - currently use browser console)
# Call via JavaScript console:
fetch('/webauthn/register/start', { method: 'POST' })
.then(r => r.json())
.then(data => {
// Use browser's navigator.credentials.create() with returned options
});
```
**3. Test Passkey Authentication:**
- Navigate to `/login`
- Click on username field
- Browser should show passkey autofill (Chrome 108+, Safari 16+)
- Select a passkey to authenticate
**4. Test Admin-Enforced 2FA:**
```graphql
# Via admin API (port 9091)
mutation {
setUser2faRequired(username: "admin", required: true) {
success
}
}
```
Then:
1. Log out
2. Log in with password
3. Should redirect to `/login/2fa`
4. Complete passkey verification
5. Should complete authorization with ACR="aal2"
**5. Test Context-Based 2FA:**
```bash
# Request authorization with max_age < 300
curl "http://localhost:9090/authorize?...&max_age=60"
# Should trigger 2FA even if not admin-enforced
```
### OIDC Flow Testing
```bash
# 1. Register a client
curl -X POST http://localhost:9090/connect/register \
-H "Content-Type: application/json" \
-d '{
"redirect_uris": ["http://localhost:8080/callback"],
"client_name": "Test Client"
}'
# 2. Generate PKCE
verifier=$(openssl rand -base64 32 | tr -d '=' | tr '+/' '-_')
challenge=$(echo -n "$verifier" | openssl dgst -binary -sha256 | base64 | tr -d '=' | tr '+/' '-_')
# 3. Navigate to authorize endpoint (in browser)
http://localhost:9090/authorize?client_id=CLIENT_ID&redirect_uri=http://localhost:8080/callback&response_type=code&scope=openid&code_challenge=$challenge&code_challenge_method=S256&state=random
# 4. After redirect, exchange code for tokens
curl -X POST http://localhost:9090/token \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "grant_type=authorization_code&code=CODE&redirect_uri=http://localhost:8080/callback&client_id=CLIENT_ID&client_secret=SECRET&code_verifier=$verifier"
# 5. Decode ID token to verify AMR/ACR claims
# Use jwt.io or similar to inspect the token
```
### Expected ID Token Claims
After passkey authentication:
```json
{
"iss": "http://localhost:9090",
"sub": "user_subject_uuid",
"aud": "client_id",
"exp": 1234567890,
"iat": 1234564290,
"auth_time": 1234564290,
"amr": ["hwk"], // or ["swk"] for cloud-synced, ["pwd", "hwk"] for 2FA
"acr": "aal1", // or "aal2" for 2FA
"nonce": "optional_nonce"
}
```
## Migration Guide for Existing Deployments
If you have an existing Barycenter deployment, the database will be automatically migrated when you update:
1. **Backup your database** before upgrading
2. Run the application - migrations run automatically on startup
3. New tables will be created:
- `passkeys`: Stores registered passkeys
- `webauthn_challenges`: Temporary challenge storage
4. Existing tables will be extended:
- `sessions`: Added `amr`, `acr`, `mfa_verified` columns
- `users`: Added `requires_2fa`, `passkey_enrolled_at` columns
**Post-Migration Steps:**
1. Build the WASM client:
```bash
cd client-wasm
wasm-pack build --target web --out-dir ../static/wasm
```
2. Restart the application to serve static files
3. Users can now register passkeys via `/account/passkeys` (future UI)
4. Enable 2FA for specific users via admin API:
```graphql
mutation {
setUser2faRequired(username: "admin", required: true) {
success
}
}
```
**No Breaking Changes:**
- Password authentication continues to work
- Existing sessions remain valid
- ID tokens now include AMR/ACR claims (additive change)
- OIDC clients receiving new claims should handle gracefully