Add RBAC with role-based endpoint authorization and OIDC support
Implement three-phase security upgrade: Phase 1 - RBAC: Extend JWT with roles claim, populate Spring GrantedAuthority in filter, enforce role-based access (AGENT for data/heartbeat/SSE, VIEWER+ for search/diagrams, OPERATOR+ for commands, ADMIN for user management). Configurable JWT secret via CAMELEER_JWT_SECRET env var for token persistence across restarts. Phase 2 - User persistence: ClickHouse users table with ReplacingMergeTree, UserRepository interface + ClickHouse impl, UserAdminController for CRUD at /api/v1/admin/users. Local login upserts user on each authentication. Phase 3 - OIDC: Token exchange flow where SPA sends auth code, server exchanges it server-side (keeping client_secret secure), validates id_token via JWKS, resolves roles (DB override > OIDC claim > default), issues internal JWT. Conditional on CAMELEER_OIDC_ENABLED=true. Uses oauth2-oidc-sdk for standards compliance. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,48 +1,60 @@
|
||||
package com.cameleer3.server.core.security;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Service for creating and validating JSON Web Tokens (JWT).
|
||||
* <p>
|
||||
* Access tokens are short-lived (default 1 hour) and used for API authentication.
|
||||
* Refresh tokens are longer-lived (default 7 days) and used to obtain new access tokens.
|
||||
* Tokens carry a {@code roles} claim for role-based access control.
|
||||
*/
|
||||
public interface JwtService {
|
||||
|
||||
/**
|
||||
* Creates a signed access JWT with the given agent ID and group.
|
||||
* Validated JWT payload.
|
||||
*
|
||||
* @param agentId the agent identifier (becomes the {@code sub} claim)
|
||||
* @param group the agent group (becomes the {@code group} claim)
|
||||
* @return a signed JWT string
|
||||
* @param subject the {@code sub} claim (agent ID or {@code ui:<username>})
|
||||
* @param group the {@code group} claim
|
||||
* @param roles the {@code roles} claim (e.g. {@code ["AGENT"]}, {@code ["ADMIN"]})
|
||||
*/
|
||||
String createAccessToken(String agentId, String group);
|
||||
record JwtValidationResult(String subject, String group, List<String> roles) {}
|
||||
|
||||
/**
|
||||
* Creates a signed refresh JWT with the given agent ID and group.
|
||||
*
|
||||
* @param agentId the agent identifier (becomes the {@code sub} claim)
|
||||
* @param group the agent group (becomes the {@code group} claim)
|
||||
* @return a signed JWT string
|
||||
* Creates a signed access JWT with the given subject, group, and roles.
|
||||
*/
|
||||
String createRefreshToken(String agentId, String group);
|
||||
String createAccessToken(String subject, String group, List<String> roles);
|
||||
|
||||
/**
|
||||
* Validates an access token and extracts the agent ID.
|
||||
* Rejects expired tokens and tokens that are not of type "access".
|
||||
* Creates a signed refresh JWT with the given subject, group, and roles.
|
||||
*/
|
||||
String createRefreshToken(String subject, String group, List<String> roles);
|
||||
|
||||
/**
|
||||
* Validates an access token and returns the full validation result.
|
||||
*
|
||||
* @param token the JWT string to validate
|
||||
* @return the agent ID from the {@code sub} claim
|
||||
* @throws InvalidTokenException if the token is invalid, expired, or not an access token
|
||||
*/
|
||||
String validateAndExtractAgentId(String token);
|
||||
JwtValidationResult validateAccessToken(String token);
|
||||
|
||||
/**
|
||||
* Validates a refresh token and extracts the agent ID.
|
||||
* Rejects expired tokens and tokens that are not of type "refresh".
|
||||
* Validates a refresh token and returns the full validation result.
|
||||
*
|
||||
* @param token the JWT string to validate
|
||||
* @return the agent ID from the {@code sub} claim
|
||||
* @throws InvalidTokenException if the token is invalid, expired, or not a refresh token
|
||||
*/
|
||||
String validateRefreshToken(String token);
|
||||
JwtValidationResult validateRefreshToken(String token);
|
||||
|
||||
// --- Backward-compatible defaults (delegate to role-aware methods) ---
|
||||
|
||||
default String createAccessToken(String subject, String group) {
|
||||
return createAccessToken(subject, group, List.of());
|
||||
}
|
||||
|
||||
default String createRefreshToken(String subject, String group) {
|
||||
return createRefreshToken(subject, group, List.of());
|
||||
}
|
||||
|
||||
default String validateAndExtractAgentId(String token) {
|
||||
return validateAccessToken(token).subject();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
package com.cameleer3.server.core.security;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Represents a persisted user in the system.
|
||||
*
|
||||
* @param userId unique identifier (e.g. OIDC {@code sub} or {@code ui:<username>})
|
||||
* @param provider authentication provider ({@code "local"}, {@code "oidc:<issuer-host>"})
|
||||
* @param email user email (may be empty)
|
||||
* @param displayName display name (may be empty)
|
||||
* @param roles assigned roles (e.g. {@code ["ADMIN"]}, {@code ["VIEWER"]})
|
||||
* @param createdAt first creation timestamp
|
||||
*/
|
||||
public record UserInfo(
|
||||
String userId,
|
||||
String provider,
|
||||
String email,
|
||||
String displayName,
|
||||
List<String> roles,
|
||||
Instant createdAt
|
||||
) {}
|
||||
@@ -0,0 +1,20 @@
|
||||
package com.cameleer3.server.core.security;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* Persistence interface for user management.
|
||||
*/
|
||||
public interface UserRepository {
|
||||
|
||||
Optional<UserInfo> findById(String userId);
|
||||
|
||||
List<UserInfo> findAll();
|
||||
|
||||
void upsert(UserInfo user);
|
||||
|
||||
void updateRoles(String userId, List<String> roles);
|
||||
|
||||
void delete(String userId);
|
||||
}
|
||||
Reference in New Issue
Block a user