TripQlub Platform — Database-Driven API Access Management¶
Overview¶
This document describes the design for managing API access rules (which roles can access which APIs) and module release status through a database-driven system with an admin UI, replacing the previous property-file approach.
Goals¶
- Eliminate configuration drift between environments — each environment's database holds its own access rules natively.
- Prevent forgotten API registrations — new APIs are auto-discovered and registered at application startup.
- Control module releases per environment — unreleased modules are invisible in production regardless of whether the code is deployed.
- Provide auditability — all changes to access rules are logged with who changed what and when.
- Enable runtime changes — role assignments can be updated without redeployment.
Database Schema¶
Tables¶
api_modules¶
Tracks each backend module and its release status per environment.
CREATE TABLE api_modules (
module_name VARCHAR(100) PRIMARY KEY,
display_name VARCHAR(255),
description TEXT,
is_released BOOLEAN NOT NULL DEFAULT FALSE,
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP NOT NULL DEFAULT NOW()
);
api_access_rules¶
Stores access rules for each individual API endpoint.
CREATE TABLE api_access_rules (
api_name VARCHAR(255) PRIMARY KEY,
module_name VARCHAR(100) NOT NULL REFERENCES api_modules(module_name),
allowed_roles TEXT NOT NULL,
is_active BOOLEAN NOT NULL DEFAULT FALSE,
is_stale BOOLEAN NOT NULL DEFAULT FALSE,
default_roles TEXT NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_api_access_module ON api_access_rules(module_name);
CREATE INDEX idx_api_access_active ON api_access_rules(is_active);
api_access_audit_log¶
Immutable log of all changes made to modules and API access rules.
CREATE TABLE api_access_audit_log (
id SERIAL PRIMARY KEY,
api_name VARCHAR(255),
module_name VARCHAR(100),
field_changed VARCHAR(50) NOT NULL,
old_value TEXT,
new_value TEXT,
changed_by VARCHAR(100) NOT NULL,
changed_at TIMESTAMP NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_audit_api ON api_access_audit_log(api_name);
CREATE INDEX idx_audit_module ON api_access_audit_log(module_name);
CREATE INDEX idx_audit_time ON api_access_audit_log(changed_at);
Column Reference¶
| Table | Column | Purpose |
|---|---|---|
api_modules |
is_released |
Controls whether the entire module's APIs are available in this environment |
api_access_rules |
allowed_roles |
Comma-separated list of roles permitted to call this API |
api_access_rules |
is_active |
Fine-grained control — allows disabling individual APIs within a released module |
api_access_rules |
is_stale |
Flagged automatically when an API exists in the DB but is no longer found in code |
api_access_rules |
default_roles |
The roles declared in code via annotation — stored for reference, not used at runtime |
Flyway Migration Script¶
This schema should be added as a Flyway migration (e.g., V010__create_api_access_tables.sql):
-- V010__create_api_access_tables.sql
CREATE TABLE api_modules (
module_name VARCHAR(100) PRIMARY KEY,
display_name VARCHAR(255),
description TEXT,
is_released BOOLEAN NOT NULL DEFAULT FALSE,
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP NOT NULL DEFAULT NOW()
);
CREATE TABLE api_access_rules (
api_name VARCHAR(255) PRIMARY KEY,
module_name VARCHAR(100) NOT NULL REFERENCES api_modules(module_name),
allowed_roles TEXT NOT NULL,
is_active BOOLEAN NOT NULL DEFAULT FALSE,
is_stale BOOLEAN NOT NULL DEFAULT FALSE,
default_roles TEXT NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_api_access_module ON api_access_rules(module_name);
CREATE INDEX idx_api_access_active ON api_access_rules(is_active);
CREATE TABLE api_access_audit_log (
id SERIAL PRIMARY KEY,
api_name VARCHAR(255),
module_name VARCHAR(100),
field_changed VARCHAR(50) NOT NULL,
old_value TEXT,
new_value TEXT,
changed_by VARCHAR(100) NOT NULL,
changed_at TIMESTAMP NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_audit_api ON api_access_audit_log(api_name);
CREATE INDEX idx_audit_module ON api_access_audit_log(module_name);
CREATE INDEX idx_audit_time ON api_access_audit_log(changed_at);
API Annotation¶
Each API endpoint declares its module and default roles using an annotation. This keeps access metadata co-located with the code and visible during development.
Annotation Definition¶
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface ApiAccess {
/** The module this API belongs to */
String module();
/** Default roles allowed to access this API */
String[] defaultRoles();
/** Optional description for the admin UI */
String description() default "";
}
Usage on Endpoints¶
@ApiAccess(module = "bookings", defaultRoles = {"AGENT", "ADMIN"})
public Response createBooking(Request req) { ... }
@ApiAccess(module = "bookings", defaultRoles = {"AGENT", "ADMIN", "CUSTOMER"})
public Response listBookings(Request req) { ... }
@ApiAccess(module = "bookings", defaultRoles = {"AGENT", "ADMIN"})
public Response cancelBooking(Request req) { ... }
@ApiAccess(module = "ai-planner", defaultRoles = {"AGENT", "ADMIN"})
public Response suggestItinerary(Request req) { ... }
@ApiAccess(module = "ai-planner", defaultRoles = {"AGENT", "ADMIN"})
public Response generateTrip(Request req) { ... }
@ApiAccess(module = "ai-planner", defaultRoles = {"AGENT", "ADMIN", "CUSTOMER"})
public Response submitFeedback(Request req) { ... }
Auto-Registration at Startup¶
On application startup, the system scans all annotated API endpoints and synchronizes them with the database. This ensures new APIs are always registered without manual intervention, while never overwriting existing configuration.
Startup Sync Logic¶
@OnStartup
public class ApiRegistrySync {
private final ApiAccessRuleRepository accessRuleRepo;
private final ApiModuleRepository moduleRepo;
private final ApiEndpointScanner scanner;
private final AppConfig config;
public void synchronize() {
Set<ApiEndpoint> declaredApis = scanner.scanAnnotatedEndpoints();
// 1. Ensure all modules exist
Set<String> declaredModules = declaredApis.stream()
.map(ApiEndpoint::getModule)
.collect(Collectors.toSet());
for (String moduleName : declaredModules) {
if (!moduleRepo.exists(moduleName)) {
moduleRepo.create(moduleName, config.isAutoActivate());
log.info("Registered new module: {} (released={})",
moduleName, config.isAutoActivate());
}
}
// 2. Register new APIs
for (ApiEndpoint api : declaredApis) {
if (!accessRuleRepo.exists(api.getName())) {
boolean activate = config.isAutoActivate();
accessRuleRepo.create(
api.getName(),
api.getModule(),
String.join(", ", api.getDefaultRoles()), // allowed_roles
String.join(", ", api.getDefaultRoles()), // default_roles
activate // is_active
);
log.info("Registered new API: {} [module={}, active={}]",
api.getName(), api.getModule(), activate);
}
}
// 3. Detect stale APIs (in DB but no longer in code)
Set<String> codeApiNames = declaredApis.stream()
.map(ApiEndpoint::getName)
.collect(Collectors.toSet());
Set<String> dbApiNames = accessRuleRepo.getAllApiNames();
Set<String> staleApis = new HashSet<>(dbApiNames);
staleApis.removeAll(codeApiNames);
if (!staleApis.isEmpty()) {
accessRuleRepo.markStale(staleApis);
log.warn("Stale APIs detected (in DB but not in code): {}", staleApis);
}
// 4. Clear stale flag for APIs that are back in code
Set<String> restoredApis = new HashSet<>(codeApiNames);
restoredApis.retainAll(dbApiNames);
accessRuleRepo.clearStaleFlag(restoredApis);
}
}
Environment-Specific Behavior¶
Controlled by a single application property:
# Development environments: auto-activate new APIs and modules
api.access.auto-activate = true
# Production / staging: register but leave inactive pending manual activation
api.access.auto-activate = false
| Environment | auto-activate |
New Module | New API |
|---|---|---|---|
| Dev (laptops, home lab) | true |
Released automatically | Active automatically |
| Test / Stage | false |
Registered, not released | Registered, not active |
| Production | false |
Registered, not released | Registered, not active |
Runtime Access Check¶
Access Rule Enforcement¶
The access check runs on every API call. Since access rules are checked frequently, they are cached in memory and refreshed periodically.
public class ApiAccessFilter {
private volatile Map<String, Set<String>> cachedRules = new ConcurrentHashMap<>();
private volatile Set<String> releasedModules = ConcurrentHashMap.newKeySet();
/**
* Checks whether a given role is allowed to access the specified API.
* Returns true if access is granted, false otherwise.
*/
public boolean isAllowed(String apiName, String userRole) {
// 1. Extract module from API name: "api.bookings.create" → "bookings"
String module = extractModule(apiName);
// 2. Check if the module is released
if (!releasedModules.contains(module)) {
return false;
}
// 3. Check if the API is active and the role is allowed
Set<String> allowedRoles = cachedRules.get(apiName);
if (allowedRoles == null) {
return false; // API not registered or not active
}
return allowedRoles.contains(userRole);
}
private String extractModule(String apiName) {
// "api.bookings.create" → "bookings"
String[] parts = apiName.split("\\.", 3);
return parts.length >= 2 ? parts[1] : "";
}
}
Cache Refresh¶
/**
* Refreshes the in-memory cache from the database.
* Runs on a scheduled interval and can be triggered on demand
* via the admin API.
*/
@Scheduled(fixedRate = 60_000) // every 60 seconds
public void refreshCache() {
// Load only active APIs in released modules
Map<String, Set<String>> newRules = accessRuleRepo.loadActiveRulesInReleasedModules();
Set<String> newModules = moduleRepo.loadReleasedModuleNames();
this.cachedRules = newRules;
this.releasedModules = newModules;
log.debug("Access rules cache refreshed: {} APIs in {} modules",
newRules.size(), newModules.size());
}
The 60-second refresh means that changes made through the admin UI take effect within one minute across all backend instances, without any restart or redeployment.
Force Refresh Endpoint¶
For immediate effect after admin changes:
@ApiAccess(module = "admin", defaultRoles = {"ADMIN"})
public Response forceRefreshAccessRules(Request req) {
refreshCache();
return Response.ok("Cache refreshed: " + cachedRules.size() + " active rules");
}
Admin UI¶
The admin interface provides two main screens for managing modules and API access rules, plus an audit log viewer.
Module Management Screen¶
Displays all registered modules with their release status.
| Module | Display Name | Released | APIs (Active / Total) | Actions |
|---|---|---|---|---|
| bookings | Bookings | ✅ Yes | 12 / 12 | Toggle, Edit |
| itineraries | Itineraries | ✅ Yes | 8 / 8 | Toggle, Edit |
| ai-planner | AI Trip Planner | ❌ No | 0 / 6 | Toggle, Edit |
| b2b-connect | B2B Connect | ❌ No | 0 / 4 | Toggle, Edit |
Actions:
- Toggle release — flips
is_released. When releasing a module for the first time, prompts whether to also activate all its APIs or review them individually. - Edit — update display name and description.
API Access Rules Screen¶
Filterable by module, with indicators for status.
| API Name | Module | Allowed Roles | Active | Stale | Actions |
|---|---|---|---|---|---|
| api.bookings.create | bookings | AGENT, ADMIN | ✅ | — | Edit |
| api.bookings.list | bookings | AGENT, ADMIN, CUSTOMER | ✅ | — | Edit |
| api.ai-planner.suggest | ai-planner | AGENT, ADMIN | ❌ | — | Edit |
| api.legacy.old-endpoint | legacy | ADMIN | ✅ | ⚠️ | Edit, Remove |
Actions:
- Edit roles — modify the
allowed_roleslist. Shows thedefault_rolesfor reference so the admin can see what the code originally declared. - Toggle active — enable/disable an individual API within a released module.
- Remove stale — delete APIs that no longer exist in code (flagged with ⚠️).
Audit Log Screen¶
Searchable and filterable log of all changes.
| Timestamp | Changed By | API / Module | Field | Old Value | New Value |
|---|---|---|---|---|---|
| 2025-02-07 14:30 | admin@agency | api.bookings.create | allowed_roles | AGENT, ADMIN | SENIOR_AGENT, ADMIN |
| 2025-02-07 14:28 | admin@agency | module: ai-planner | is_released | false | true |
| 2025-02-07 10:15 | SYSTEM (startup) | api.ai-planner.feedback | — | — | Created (inactive) |
Workflow Scenarios¶
Scenario 1: New API Added to an Existing Released Module¶
- Developer adds a new endpoint with
@ApiAccess(module = "bookings", ...)annotation. - Code is committed and deployed through the TeamCity pipeline.
- On application startup, the sync process detects the new API.
- In dev/home lab (
auto-activate = true): The API is registered and immediately active with default roles. Developer can use it right away. - In production (
auto-activate = false): The API is registered but inactive. It appears in the admin UI. An admin reviews it, adjusts roles if needed, and activates it.
Scenario 2: New Module Under Development¶
- Developer creates a new module (e.g.,
ai-planner) with multiple endpoints, all annotated. - Code is deployed to all environments.
- In dev: Module is auto-released, all APIs auto-activated. Full functionality available for testing.
- In production: Module is registered but not released. None of its APIs are accessible, regardless of their individual
is_activestatus. The module is invisible to production users. - When ready to launch: Admin opens the admin UI, releases the module, reviews and activates its APIs with appropriate production roles.
Scenario 3: Restricting an Existing API in Production¶
- Admin opens the API Access Rules screen in the production admin UI.
- Finds the API (e.g.,
api.bookings.create), clicks Edit. - Changes
allowed_rolesfromAGENT, ADMINtoSENIOR_AGENT, ADMIN. - Saves. The change is logged in the audit trail.
- Within 60 seconds (or immediately via force refresh), the new roles take effect.
- No redeployment needed.
Scenario 4: API Removed from Code¶
- Developer removes an endpoint from the codebase.
- On next deployment, the startup sync flags the API as stale (
is_stale = true). - The API appears with a ⚠️ warning in the admin UI.
- Admin reviews and either removes it from the database or leaves it (it's already inactive if it's not in code).
Scenario 5: Role Requirements Differ Between Environments¶
This is handled naturally — each environment's database has its own allowed_roles values. When a new API is registered with default roles via auto-activate, those defaults serve as a starting point. Production admins can adjust roles at any time through the admin UI without affecting other environments.
Access Resolution Flow¶
Incoming API Request
│
▼
┌─────────────┐
│ Extract API │ e.g., "api.bookings.create"
│ name & role │ e.g., role = "AGENT"
└──────┬──────┘
│
▼
┌─────────────────┐ No
│ Module released? │──────────▶ 403 Forbidden
│ (in-memory cache)│
└──────┬──────────┘
│ Yes
▼
┌─────────────────┐ No
│ API is active? │──────────▶ 403 Forbidden
│ (in-memory cache)│
└──────┬──────────┘
│ Yes
▼
┌─────────────────┐ No
│ Role in allowed │──────────▶ 403 Forbidden
│ roles list? │
└──────┬──────────┘
│ Yes
▼
200 OK / Proceed
Configuration Summary¶
Application Properties¶
# Environment identifier
app.environment = prod
# Auto-activation behavior
# true = new modules released, new APIs activated on startup (dev)
# false = new modules/APIs registered but inactive on startup (prod, stage)
api.access.auto-activate = false
# Cache refresh interval in milliseconds
api.access.cache-refresh-ms = 60000
Per-Environment Settings¶
| Property | Dev (laptops, home lab) | Test / Stage | Production |
|---|---|---|---|
app.environment |
dev |
stage |
prod |
api.access.auto-activate |
true |
false |
false |
api.access.cache-refresh-ms |
10000 |
60000 |
60000 |
Integration with TeamCity Build Pipeline¶
The API access management system requires no special build steps — it is fully automated at application startup. However, the TeamCity pipeline can include an optional verification step:
Post-Deployment Verification (Optional)¶
After deploying the backend, add a build step that queries the admin API to report on unactivated APIs:
#!/bin/bash
# Check for new APIs pending activation in the target environment
RESPONSE=$(curl -s -H "Authorization: Bearer ${ADMIN_TOKEN}" \
"https://${BACKEND_HOST}/admin/api-access/pending")
PENDING_COUNT=$(echo "$RESPONSE" | jq '.pending | length')
if [ "$PENDING_COUNT" -gt 0 ]; then
echo "##teamcity[message text='${PENDING_COUNT} new APIs pending activation' status='WARNING']"
echo "$RESPONSE" | jq -r '.pending[] | " - \(.api_name) [\(.module_name)]"'
fi
This provides visibility in TeamCity when new APIs have been deployed but not yet activated in the target environment.
Security Considerations¶
- The admin UI must be protected by the existing Keycloak/OAuth2 authentication with an
ADMINorSUPER_ADMINrole requirement. - The audit log table should be append-only — no UPDATE or DELETE permissions for the application user; use a separate DB role for audit log maintenance if cleanup is ever needed.
- The force-refresh endpoint should be restricted to admin roles to prevent cache manipulation.
- Database credentials for the access rules tables should follow the same security practices as other sensitive configuration (TeamCity password parameters, environment-specific secrets).