Skip to content

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

  1. Validate token with CI API
  2. Extract username + tokenExpireAt
  3. Check working hours → deny if outside window
  4. Load all active (non-revoked) sessions for username
  5. Exclude current tokenKey (re-login with same token)
  6. While sessions.size() >= maxSessions: evict oldest (revokeByToken)
  7. Upsert current session: update lastActiveAt/lastVerifiedAt if session exists, otherwise insert new UserSession row

Non-Login Endpoint Flow

  1. Lookup session by tokenKey
  2. Reject with 401 if session is not found, revoked, or token is expired
  3. Re-verify with CI API if now − lastVerifiedAt >= verifyIntervalMinutes; revoke and return 401 on failure
  4. Check working hours → deny with 403 if outside window
  5. Check privilege level → deny with 403 if insufficient
  6. 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