DEV Community

ZekeCross3245
ZekeCross3245

Posted on

Batch User Directory Reads: Revoking Gaming Sessions Without Authorization Drift

Short answer: Use a policy-filtered, paginated directory read for discovery, then require a fresh per-user authorization check for refresh-token rotation or revocation.

Listing a game’s accounts is an administrative operation, not a shortcut around each player’s authorization. For batch user operations, keep the directory query narrow, bind every returned record to an explicit policy decision, and rotate or revoke refresh tokens through a separate command path. That separation preserves session security while keeping operator friction predictable.

The bill starts with retention, not the query

The expensive part of a directory feature is usually the data it keeps around after the page has loaded: copied profiles, exported token metadata, audit payloads, and ad hoc caches. A gaming support team may search 50,000 accounts during an event, but retaining a full account snapshot for every search creates a larger security and storage surface than the read itself. Picture an outage review after a tournament: three operators export overlapping CSV files, a queue worker mirrors them into a temporary bucket, and a dashboard caches the same rows for its auto-refresh. Each copy extends the period in which a leaked operator credential can reveal player details, and each copy becomes another deletion job to prove. Keep only identifiers, policy-relevant attributes, and an audit reference; fetch the minimum display fields for the current page, and set the export worker to discard its intermediate file after the handoff rather than treating storage as a free extension of the database.

I once treated a CSV export as harmless because it contained no passwords. Then I counted the columns: email, country, parental-control state, last-login time, and a refresh-token family identifier. A 30-day object-retention rule turned a five-minute investigation into a month of recoverable personal data. The fix was not a faster database. It was a shorter retention window and a redacted export schema.

That choice has a cost. When an incident is older than the retained audit payload, investigators lose convenient context and must reconstruct events from immutable logs. I accept that trade for routine support work; a fraud investigation with a legal hold should use a separately approved retention policy.

How can batch user directory listing preserve per-user authorization?

Start with two identities in the request: the operator identity and the target account identity. The operator may have permission to discover a limited set of accounts, while actions on each account still require a per-user decision. A list endpoint should return policy-filtered rows, never a broad result followed by client-side hiding.

The policy input should be stable and boring: tenant or shard, support role, purpose, and the requested fields. Do not infer permission from a search string, a UI route, or a guessed account status. For each row, evaluate access again before issuing a session command, because a role can change between the list read and the click.

Here is a small service boundary. It is intentionally generic: the directory store supplies rows, and the policy engine decides what the operator can see or do.

from dataclasses import dataclass
from typing import Iterable


@dataclass(frozen=True)
class Account:
    account_id: str
    shard: str
    email: str
    risk_state: str


def list_accounts(operator, query, directory, policy) -> list[dict]:
    rows: Iterable[Account] = directory.search(
        shard=operator.shard,
        text=query.text,
        limit=min(query.limit, 100),
    )
    visible = []
    for account in rows:
        decision = policy.check(
            subject=operator.id,
            action="account.read",
            resource=account.account_id,
            purpose=query.purpose,
        )
        if decision.allow:
            visible.append({
                "account_id": account.account_id,
                "email": account.email,
                "risk_state": account.risk_state,
            })
    return visible


def revoke_refresh_family(operator, account_id, directory, policy, sessions):
    decision = policy.check(
        subject=operator.id,
        action="session.revoke",
        resource=account_id,
        purpose="stolen-session-response",
    )
    if not decision.allow:
        raise PermissionError("per-user authorization denied")
    sessions.revoke_refresh_token_family(account_id)
    directory.audit(operator.id, account_id, "session.revoke")
Enter fullscreen mode Exit fullscreen mode

The revoke operation is idempotent: repeating it leaves the token family unusable and records a second audit event with its own request ID. A refresh token rotation policy should also reject reuse of an old token, invalidate the related family when reuse is detected, and require reauthentication according to the risk policy. Those controls reduce the window in which a stolen session can be replayed; they do not grant the operator broader directory access.

Failure modes that look like convenience features

The dangerous designs are often the ones that make a support queue feel fast. A wildcard search that returns every shard leaks account existence. A list response that includes refresh-token identifiers turns a read permission into a credential-adjacent capability. A revoke button that trusts a hidden HTML field can act on a different account than the one the operator reviewed.

Another trap is caching authorization with the directory page. A five-minute cache may be acceptable for display text, but it isn't a valid grant for revocation. Cache rows if needed; re-check policy and account state at command time. Log the policy version, subject, target, purpose, and outcome, while keeping token values and unnecessary profile data out of the log.

Short timeout. Clear retry rules.

Retries deserve care because a network timeout does not tell the caller whether revocation committed. Use an idempotency key for the command, persist its outcome, and let the operator see “already revoked” as a successful state rather than inviting repeated clicks. Your mileage may vary on the exact timeout: measure it against the session service’s lease and the support workflow, not a generic default.

Choosing the boundary for security versus friction

The right design depends on who operates the tool, how quickly a stolen session must be cut off, and how much account data the team can legitimately inspect. I use this decision table before choosing a directory API shape:

Situation Safer default Friction introduced When to choose another path
Routine support lookup Policy-filtered, paginated fields More policy checks per row Use a pre-approved queue for very high-volume triage
Confirmed stolen session Separate per-user revoke command with rotation Operator must confirm the target Use an automated risk trigger when seconds matter
Cross-shard investigation Brokered search with shard scope Fewer results per request Use an incident role with time-bounded approval
Legal or fraud investigation Immutable audit stream and approved retention More review and storage Stick with short retention for ordinary support

The catch is that a strongly isolated workflow is not suitable when a game has no staffed response team and automated abuse detection must act immediately. In that case, keep the human directory narrow and let a machine policy revoke a session from a risk event, with the same audit contract. Conversely, do not automate broad account discovery merely to remove a few clicks.

Test the contract before shipping

Authorization tests should assert absence, not only presence. Create two operators, two shards, and accounts with similar names; verify that a denied row is absent, that a permitted row exposes only approved fields, and that changing the operator role between listing and revocation blocks the command. Add property tests for pagination boundaries and Unicode search input, then run a replay test where the same idempotency key is submitted three times.

Operationally, alert on unusual result counts, repeated denied actions, and revocations that lack a matching policy decision. Review retention jobs as carefully as database indexes: expired exports should be verifiably deleted, while records under a legal hold should be isolated from routine cleanup. OWASP’s authentication guidance emphasizes protecting session identifiers and reauthentication after risk events; the directory design should make those requirements visible in code and logs, not leave them as a checklist item.

References

Top comments (0)