CIOAuth — Admin Management, Working Hours & Session Limit
Overview ¶
This document describes three tightly coupled features built on top of the @CIOAuth annotation in the Internal Service Status (ISS) service:
| Feature | Summary |
|---|---|
| Admin User Management | Designate/revoke admin users; gate privileged API endpoints |
| Working Hours Restriction | Restrict per-user login access to configured time windows |
| Session Connection Count Limit | Enforce per-user concurrent session caps (different limits for admin vs. regular users) |
All three features are enforced as AOP advice (AnnotationAspect) that intercepts every method annotated with @CIOAuth.
Background & Motivation ¶
The @CIOAuth annotation was originally a thin Cisco Identity Broker (CI) token check. As the service matured, three control requirements were added:
- Role-based access: Certain destructive operations (add/remove admin, etc.) must be restricted to designated administrators.
- Time-based access control: Operators may need to restrict when certain users can log in (e.g., contractors limited to business hours).
- Concurrent session control: To prevent token sharing or credential abuse, each user should only maintain a bounded number of active sessions at any point in time, with admins allowed slightly more headroom.
Key Components ¶
Annotation: @CIOAuth ¶
@Inherited
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface CIOAuth {
UserPrivilegeEnum privilege() default UserPrivilegeEnum.user;
boolean loginEntry() default false;
}
| Attribute | Type | Default | Purpose |
|---|---|---|---|
privilege | UserPrivilegeEnum | user | Required privilege level (user or admin) |
loginEntry | boolean | false | Marks an endpoint as the session creation entry point |
Privilege Enum ¶
public enum UserPrivilegeEnum {
admin,
user
}
Data Models ¶
StatusAdmin — Admin Registry
| Column | Type | Description |
|---|---|---|
username | varchar(64) | CI username (PK, unique) |
created_at | timestamp | When the admin was added |
created_by | varchar(64) | Who added this admin |
remark | text | Optional note |
UserSession — Active Session Registry
| Column | Type | Description |
|---|---|---|
token | varchar(512) | SHA-256 hash of the raw CI token (PK) |
username | varchar(64) | CI username |
is_revoked | boolean | Whether this session has been forcibly evicted |
created_at | timestamp | Session creation time |
last_active_at | timestamp | Last time the session was used |
last_verified_at | timestamp | Last time the token was re-checked with CI |
token_expire_at | timestamp | CI-reported token expiry |
WorkingHours — Per-User Access Window
| Column | Type | Description |
|---|---|---|
username | varchar(64) | CI username (PK) |
start_time | time | Daily window start (e.g. 09:00) |
end_time | time | Daily window end (e.g. 18:00) |
work_days | varchar(100) | Comma-separated day names (e.g. MONDAY,TUESDAY,WEDNESDAY,THURSDAY,FRIDAY) |
timezone | varchar(100) | IANA timezone (default UTC) |
Admin User Management ¶
Allow a set of privileged "admin" users to access sensitive operations. Non-admin authenticated users are blocked from those endpoints with 403 Forbidden.
Admin status is stored in the status_admin table, managed through AdminService / StatusAdminMapper. The system is seeded with a set of default admins at DB initialisation.
AdminService
├── addAdmin(StatusAdmin) → INSERT into status_admin
├── removeAdmin(username) → DELETE from status_admin
└── isAdmin(username) → SELECT; returns true if row exists
Annotating a method with @CIOAuth(privilege = UserPrivilegeEnum.admin) requires the caller to be an admin. The AOP intercept enforces this after session validation:
isAdmin = adminService.isAdmin(username)
if (annotation.privilege() == admin && !isAdmin)
→ 403 "Admin privilege required."
API Endpoints ¶
| Method | Path | Auth Required | Admin Only |
|---|---|---|---|
POST | /user/admin | ✅ | ✅ |
DELETE | /user/admin | ✅ | ✅ |
GET | /user/admin | ✅ (loginEntry) | ❌ |
Flow ¶
Request → @CIOAuth(privilege=admin)
│
├─ Token empty? → 401
├─ Session found & valid? → continue
│
└─ isAdmin(username)?
├─ YES → proceed()
└─ NO → 403 "Admin privilege required."
Working Hours Restriction ¶
Allow per-user configuration of an allowed login window. Requests outside this window are rejected with 403 Forbidden. If no window is configured for a user, access is unrestricted.
Working hours are stored per-user in the working_hours table. The check is timezone-aware; the server converts the current UTC time to the user's configured timezone before comparing.
Check Logic (isOutsideWorkingHours) ¶
wh = workingHoursMapper.selectByUsername(username)
if (wh == null) → ALLOW (no restriction)
zone = ZoneId.of(wh.timezone ?: "UTC")
now = ZonedDateTime.now(zone)
if wh.workDays configured:
if currentDay ∉ workDays → DENY
if currentTime < wh.startTime OR currentTime > wh.endTime → DENY
→ ALLOW
The check fires on every request:
| Scenario | Timing |
|---|---|
Login entry (loginEntry=true) | Before session registration |
| Non-login endpoints | After session/token validation |
Note: The login entry checks working hours before creating a new session row, to avoid inserting dirty session data for a request that will ultimately be denied.
API Endpoints ¶
| Method | Path | Permission |
|---|---|---|
GET | /user/working-hours?username=X | Authenticated user |
POST | /user/working-hours | Self or admin |
DELETE | /user/working-hours?username=X | Self or admin |
Self-or-admin enforcement is implemented at the controller layer, not the AOP layer:
if (!adminService.isAdmin(callerUsername) && !callerUsername.equals(targetUsername))
→ 403
Session Connection Count Limit ¶
Prevent users from holding an unbounded number of concurrent active sessions (e.g., shared tokens, multiple browser tabs from different networks). When a user tries to log in beyond the limit, the oldest session is forcibly evicted (marked is_revoked = true).
Session Limits ¶
| User Role | Max Concurrent Sessions |
|---|---|
| Admin | 3 (MAX_ADMIN_SESSIONS) |
| Regular User | 2 (MAX_USER_SESSIONS) |
These constants are defined in AnnotationAspect and can be promoted to configuration if needed.
Token Security ¶
The raw CI token is never persisted. Only a SHA-256 hash (hashToken(rawToken)) is stored as the session key. The raw token is kept in memory only for the duration of a single request and used for CI re-verification.
Session Lifecycle ¶
Login Entry Flow
- Validate token with CI API
- Extract username +
tokenExpireAt - Check working hours → deny if outside window
- Load all active (non-revoked) sessions for username
- Exclude current
tokenKey(re-login with same token) - While
sessions.size() >= maxSessions: evict oldest (revokeByToken) - Upsert current session: update
lastActiveAt/lastVerifiedAtif session exists, otherwise insert newUserSessionrow
Non-Login Endpoint Flow
- Lookup session by
tokenKey - Reject with
401if session is not found, revoked, or token is expired - Re-verify with CI API if
now − lastVerifiedAt >= verifyIntervalMinutes; revoke and return401on failure - Check working hours → deny with
403if outside window - Check privilege level → deny with
403if insufficient proceed()
Eviction Strategy ¶
Sessions are ordered by last_active_at ASC (least recently used first). On overflow, the LRU session is evicted in a while loop until the count is within the limit. This gives admins natural headroom for operational workflows that require multiple simultaneous sessions.
Periodic Re-Verification ¶
To avoid calling the CI API on every request (high latency), sessions are re-verified lazily:
app.session.verify-interval-minutes: 5 # default
If now − lastVerifiedAt ≥ interval, the token is re-checked with CI. A failed re-check immediately revokes the session.
AOP Execution Order ¶
Multiple aspects apply to @CIOAuth-annotated methods:
| Aspect | @Order | Role |
|---|---|---|
AnnotationAspect | 1 (first) | Auth, session, working hours, privilege |
UserActivityAspect | 2 | Audit log (runs after a successful response) |
Data Flow ¶
HTTP Request
│
▼
@CIOAuth AOP (AnnotationAspect, Order=1)
│
├─ [Global switch off?] ──────────────────────────► proceed()
│
├─ Token missing? ───────────────────────────────► 401
│
├─ loginEntry=true (Login)
│ ├─ Call CI API (validate token) ──────────► 401 if invalid
│ ├─ Extract username, expiry
│ ├─ Check working hours ───────────────────► 403 if outside window
│ ├─ Enforce session limit (evict LRU)
│ └─ Register/refresh session in DB
│
└─ loginEntry=false (Other endpoints)
├─ Lookup session by token hash
├─ Session missing/revoked/expired? ──────► 401
├─ Periodic CI re-verify if needed ───────► 401 if invalid
└─ Check working hours ───────────────────► 403 if outside window
│
▼
Privilege check
├─ privilege=admin && !isAdmin? ─────────────────► 403
│
▼
proceed() → Controller
│
▼
UserActivityAspect (Order=2) → Audit Log
Configuration Reference ¶
| Property | Default | Description |
|---|---|---|
app.common.enableCIOAuth | true | Master switch; set to false to bypass all auth (dev only) |
app.session.verify-interval-minutes | 5 | How often (in minutes) to re-check a session's token with CI |