DEV Community

Hwpgsd503817
Hwpgsd503817

Posted on

Go Backoffice Runbook for Exact Player Lookup, Profile Edits, and Controlled Removal

Go Backoffice Runbook for Exact Player Lookup, Profile Edits, and Controlled Removal

Short answer: keep phone-OTP account operations behind a small, audited Go service that separates exact lookup, field-level profile edits, and a two-step deletion request; require an operator reason, a fresh authorization check, and an idempotency key for every mutation. The deciding constraint in a game is abuse resistance: an admin console is another attack surface, and a convenient search endpoint can become an account-enumeration tool.

A useful runbook starts with the signal, not the button. A spike in failed OTP attempts, a player reporting a changed phone number, or a moderation case that needs account removal should produce an immutable case ID before anyone opens a record. The case carries the operator identity, ticket reference, target player ID, normalized phone hash, old and new values, reason code, and expiry. Raw phone numbers should stay out of logs and analytics.

How should operators handle exact lookup, profile updates, and controlled deletion?

Treat the three operations as different risk classes. Exact lookup is read-only but still sensitive; profile edits can redirect login; deletion is an irreversible business event even when storage erasure is asynchronous. A single “edit user” endpoint encourages confused-deputy bugs, where a support role receives a capability intended for an account-recovery specialist. Separate routes, permissions, and audit event types make review possible.

For lookup, accept a canonical player ID as the primary key. A phone number can be a secondary input only after normalization and an exact match, with a generic “no matching account” response so an attacker cannot distinguish an unregistered number from a protected one. Return the minimum fields needed for the case: status, region, OTP lock state, and a masked destination. Do not return the OTP secret, recovery codes, or a complete device history.

For a profile edit, require the caller to name each field and send the expected version observed during review. The service rejects a stale version with HTTP 409 instead of silently overwriting a newer change. Changing a phone number should invalidate outstanding OTP challenges, create a security event, and trigger the product’s normal re-verification policy; it should not be a free-form database update from the console.

Deletion needs a request and an execution phase. The request records scope, legal-retention flags, and a deadline. A second, separately authorized action confirms it. At execution time, the worker rechecks the version and retention decision, marks the account pending deletion, revokes sessions, and emits an outbox event. Consumers then remove or anonymize their own records. If a retention rule blocks removal, the state is “held,” with a visible reason, rather than a partial success that operators mistake for completion.

Here is the shape I use at the service boundary. It is deliberately boring: typed commands, an idempotency key, and an audit writer that commits with the mutation.

type EditPhoneCommand struct {
    PlayerID       string
    ExpectedVersion int64
    NewPhoneHash   string
    Reason         string
    CaseID         string
    IdempotencyKey string
}

func (s *Service) EditPhone(ctx context.Context, cmd EditPhoneCommand) error {
    if cmd.PlayerID == "" || cmd.Reason == "" || cmd.CaseID == "" {
        return ErrInvalidRequest
    }
    return s.store.WithTx(ctx, func(tx Tx) error {
        if err := tx.RequireIdempotency(cmd.IdempotencyKey); err != nil {
            return err
        }
        player, err := tx.PlayerForUpdate(cmd.PlayerID)
        if err != nil {
            return err
        }
        if player.Version != cmd.ExpectedVersion {
            return ErrConflict
        }
        if err := tx.ReplacePhoneHash(player.ID, cmd.NewPhoneHash); err != nil {
            return err
        }
        if err := tx.RevokeOpenOTPChallenges(player.ID); err != nil {
            return err
        }
        return tx.AppendAudit(AuditEvent{
            Type: "player.phone_changed", PlayerID: player.ID,
            CaseID: cmd.CaseID, Reason: cmd.Reason,
        })
    })
}
Enter fullscreen mode Exit fullscreen mode

The transaction does not send SMS. It records intent and lets an outbox consumer handle delivery, retries, and provider-specific timeouts. That boundary keeps a slow carrier from holding a database lock.

Where the failure modes hide

The common failure is not a bad SQL statement; it is an incomplete state transition. A support agent updates the phone, the OTP challenge remains valid, and an attacker finishes the old challenge. Another is a delete button that removes the primary row while inventory, chat, payments, or anti-cheat systems retain identifiers that can still authenticate a session.

Capacity planning belongs in the runbook. Size the lookup path for the busiest moderation hour, then reserve write capacity for OTP invalidations and deletion fan-out. Track p95 lookup latency, 409 conflict rate, pending-deletion age, outbox lag, and audit-write failures. An SLO such as 99.9% of authorized lookups under 300 ms is useful only if a breach pages the owning team and the console shows a degraded, read-only mode.

Rate-limit by operator, case, and target account. A 404 must not reveal whether a phone hash exists; a 429 should be expected under automation. Log authorization decisions and reason codes, never OTP values. OWASP’s Authentication Cheat Sheet also recommends generic authentication responses and careful handling of credential recovery, which applies to administrative recovery workflows as well.

Build or buy the control plane?

The decision is mostly about on-call ownership and evidence quality. A managed identity console may shorten the first integration, but its deletion semantics, audit export, and regional retention behavior still need verification. A self-hosted control plane gives precise state transitions and test fixtures, while the team owns patching, access reviews, and incident response.

Choice Fits when Trade-off to record
Self-hosted Go service Domain rules span game inventory, moderation, and retention You own paging, upgrades, and audit durability
Managed identity admin UI Standard profile fields and a small support team Verify field-level permissions, export format, and deletion hooks
Existing internal workflow tool Cases already have approvals and legal holds Custom OTP invalidation and fan-out may require extensions

Do not select on license price alone. The relevant cost is the number of people who can approve a dangerous mutation at 03:00, plus the time needed to prove what happened.

Verification and rollback before production

Test the state machine with property-based cases: repeating the same idempotency key produces one audit event; a stale version never changes a phone; a held deletion revokes nothing beyond the approved scope; and a retried outbox message is safe. Exercise authorization matrices with real role fixtures, including a support role that can read masked data but cannot edit or delete.

Run a shadow lookup against production-shaped data before enabling writes. Sample the audit stream for missing case IDs and unexpected raw numbers. During rollout, gate mutations behind a feature flag and watch the 409 rate, OTP challenge revocations, and outbox lag for at least one full moderation shift.

Rollback means disabling new mutations and draining queued work, not restoring a deleted account from an unreviewed backup. For a mistaken phone edit, use a compensating, approved edit that increments the version and records why; for deletion, restore only from a documented retention copy after legal and security approval. Your mileage may vary because regional retention law and game-account dependencies differ, so have counsel and data owners sign the actual hold policy.

Three words matter: prove the action.

References

Top comments (0)