DEV Community

CelthyrDusk7341
CelthyrDusk7341

Posted on

Postgres Analytics Workspace Access: Auditable Provisioning, Sessions, and Consent Checks

Short answer: an analytics workspace should provision a user, authorize workspace membership, validate the current consent record, and issue a bounded session as separate decisions; a forgot-password flow may recover an identity, but it must never grant workspace access by itself. The extra redirect or reauthentication is friction worth keeping because the alternative turns one recovered account into an unchecked tenant-wide credential.

This is the incident lesson I use for the design review, without pretending it came from a real outage: a B2B analyst requests a password reset, opens the email on a shared laptop, changes the password, and still has an old browser session for Workspace A. During the same minute, an administrator removes that analyst from Workspace A and invites the address to Workspace B. If the reset handler copies roles into a new session, trusts the email address as identity, or treats an old consent flag as current, the analyst can cross a boundary that each individual screen appears to enforce.

The invariant is blunt. Recovery proves control of a recovery channel; it does not prove current membership, current consent, or continued possession of every existing session.

What should analytics workspace user provisioning, session control, and consent checks prove?

They should answer four different questions. Identity says which local account completed authentication. Provisioning says whether that account exists and is eligible to be evaluated. Authorization says which workspace and operation the account may access now. Consent says which recorded purpose and policy version apply to the requested data use. A session carries references to those decisions for a limited period; it should not collapse them into a durable is_allowed bit.

That separation matters most during recovery. The OWASP Authentication Cheat Sheet recommends consistent responses for password recovery so an unauthenticated caller cannot learn whether an account exists, and it calls for reauthentication after high-risk events such as password resets. Those are boundary controls, not UI polish. The public response can remain generic while the internal audit event records a pseudonymous account reference, request ID, decision, and timestamp. Don't put reset tokens, passwords, or analytics payloads in that event.

Email is a delivery attribute in this model, not the primary key. Normalize only according to rules you can defend, keep a stable internal user ID, and make workspace membership unique on (workspace_id, user_id). A first login can create a pending user record, but activation should still wait for the required proof and policy checks. For an invite, bind the invitation to the intended workspace and role; when it is redeemed, re-read that invitation and membership inside the write transaction rather than trusting fields returned by the browser.

There is one more distinction teams routinely blur: disabling an account, removing one membership, and revoking one session are different operations. A tenant administrator removing Workspace A access should not necessarily delete the person's Workspace B membership. An account closure should terminate every active session. A suspected stolen browser may justify revoking only one session while the user reviews the others. Model those actions explicitly or the audit trail will describe intent that the enforcement layer cannot reproduce.

Treat password recovery as a security state transition

A reset link should be single-use, expire under a documented policy, be stored in a form that does not disclose the bearer value if the database is read, and lead to a fresh authentication decision. OWASP also recommends invalidating existing sessions or giving the user the option to do so after a password reset. For an analytics product, I would default to invalidating all sessions when the account has export or administration privileges; a read-only account with low-sensitivity data might use a less disruptive policy, but that should be an explicit risk decision tied to the workspace classification.

Now the bounded failure sequence becomes useful. At 09:00, the user requests recovery. At 09:02, an administrator removes membership. At 09:03, the user redeems the token. A weak implementation loaded membership at 09:00 and serializes it into a long-lived browser token. The preventative path consumes the recovery token atomically, changes the credential, advances a session epoch, and then forces the next request through current membership and consent checks. If the same token is submitted again, the domain result is reset_token_replayed; the public response still should not reveal account state.

No shortcuts.

The following Go sketch keeps those responsibilities visible. The interfaces hide storage details, while the transaction boundary makes token consumption and session invalidation one commit. AuthenticateAgain represents the separate post-reset authentication ceremony; it is deliberately not called by the reset transaction.

package recovery

import (
    "context"
    "errors"
    "time"
)

var ErrInvalidReset = errors.New("reset request is invalid")

type ResetRecord struct {
    UserID    string
    ExpiresAt time.Time
    UsedAt    *time.Time
}

type Store interface {
    InTransaction(ctx context.Context, fn func(Tx) error) error
}

type Tx interface {
    LockResetByDigest(ctx context.Context, digest []byte) (ResetRecord, error)
    ReplacePasswordHash(ctx context.Context, userID string, hash []byte) error
    MarkResetUsed(ctx context.Context, digest []byte, usedAt time.Time) error
    AdvanceSessionEpoch(ctx context.Context, userID string) error
    AppendAuditEvent(ctx context.Context, event AuditEvent) error
}

type AuditEvent struct {
    Kind      string
    SubjectID string
    RequestID string
    Occurred  time.Time
}

type Service struct {
    Store Store
    Now   func() time.Time
}

func (s Service) CompleteReset(
    ctx context.Context,
    digest []byte,
    newPasswordHash []byte,
    requestID string,
) error {
    now := s.Now()
    return s.Store.InTransaction(ctx, func(tx Tx) error {
        record, err := tx.LockResetByDigest(ctx, digest)
        if err != nil || record.UsedAt != nil || !now.Before(record.ExpiresAt) {
            return ErrInvalidReset
        }
        if err := tx.ReplacePasswordHash(ctx, record.UserID, newPasswordHash); err != nil {
            return err
        }
        if err := tx.MarkResetUsed(ctx, digest, now); err != nil {
            return err
        }
        if err := tx.AdvanceSessionEpoch(ctx, record.UserID); err != nil {
            return err
        }
        return tx.AppendAuditEvent(ctx, AuditEvent{
            Kind: "password_reset_completed", SubjectID: record.UserID,
            RequestID: requestID, Occurred: now,
        })
    })
}
Enter fullscreen mode Exit fullscreen mode

This code does not issue a session, infer a workspace, or update consent. Good. After the user authenticates again, the session service can read the new epoch, and each protected request can resolve the selected workspace against live membership. A high-volume system may use a short authorization cache, but cache invalidation then becomes part of the access-removal SLO: if the organization promises revocation within a given interval, cache lifetime, event delivery, and clock skew must fit inside that budget. I'm not sure one universal interval is defensible; the data classification, contractual commitments, and observed reauthentication burden should determine it.

Put authorization and consent on the request path

The session cookie should identify a server-controlled session and use the protections described by OWASP: encrypted transport, restricted script access, an appropriate same-site policy, rotation after authentication or privilege changes, and idle plus absolute expiration. Exact durations belong in a threat model and capacity plan, not in copied sample code. A five-minute idle limit can destroy an analyst's unsaved work, while a multi-day unrestricted session can outlive an urgent membership revocation. Measure both abandoned work and reauthentication frequency, then set separate policies for routine viewing, exports, billing changes, and tenant administration. Every workspace request should then resolve the tuple (session, user, workspace, action). The server reads the workspace identifier, confirms current membership, evaluates the required role, and only then scopes the query by workspace. The database predicate is a second boundary, not a replacement for authorization. For example, an export query should receive a workspace ID from the authorization result rather than directly from a form value. This also makes denied decisions observable without logging the report itself. Consent sits beside authorization because it answers a different question: store append-only evidence with the user, purpose, policy version, collection channel, and time; on a request that needs consent, look for evidence matching the current purpose and policy; if none exists, return a re-consent state before starting a report export or data collection job. A later policy version should create a new record, preserving the old one for audit, rather than overwriting a boolean. Consent withdrawal should stop future processing covered by that consent, while retention and deletion behavior still need their own documented rules. The request path therefore has one ordered decision with several independently auditable inputs, not several screens that happen to precede the query.

Order matters.

Watch the load this creates. If every chart fans out into twenty API calls and each call performs four uncached reads, the security design has quietly become a database capacity problem. I budget authorization checks as part of request latency, index membership by workspace and user, keep consent lookup keys narrow, and test revocation under cache pressure. The SLO is not merely “login works.” It covers how quickly access removal takes effect, how often legitimate sessions are rejected, and whether audit events can be reconciled with authorization decisions.

Buy or build the control plane?

The useful comparison is about ownership, not a logo contest. A managed identity service can reduce the amount of credential-handling code the team maintains, while an internal session and authorization layer can keep tenant semantics close to the analytics data model. Neither choice removes the obligation to test recovery, membership revocation, consent versioning, and audit completeness.

Control Build internally when Buy or adopt a managed component when Operational catch
Primary authentication and recovery Requirements are narrow and the team can sustain security review and on-call ownership Federation, multiple authenticators, or lifecycle workflows would otherwise dominate the roadmap Integration still needs generic responses, reauthentication, and incident runbooks
Workspace authorization Roles and resource rules are tightly coupled to the product schema Policy authorship spans many services and teams External policy decisions add latency and need a failure policy
Session storage Revocation timing and tenant context require direct control Standard session lifecycle is sufficient and operating the store adds little differentiation Export and admin actions may still need step-up checks
Consent evidence Purposes and policy versions are few and owned by one team Legal rules, regions, and collection channels change frequently A consent platform cannot decide workspace membership

Capacity planning changes the answer. Building means forecasting session writes, authorization reads, audit-event volume, key rotation, backup recovery, and the pager load when any of those paths degrade. Buying moves some of that work but adds dependency SLOs, contract review, data-location questions, and migration cost. Lock-in is tolerable when the boundary is narrow: keep an internal user ID, expose authentication through a small interface, and avoid leaking provider-specific claims into workspace authorization records.

The catch is that this architecture is not suitable unchanged for offline clients that cannot promptly observe revocation, shared kiosk sessions, or regulated deployments requiring organization-controlled hardware credentials. For those cases, choose shorter offline grants or none at all, require stronger reauthentication for sensitive operations, and integrate the customer's workforce identity lifecycle. Conversely, a small internal analytics tool with no external tenants may not need a full consent ledger; documented access approval and centralized workforce authentication can be the clearer control. Session security versus friction is a workload decision, not a maturity badge.

Ship the failure tests before the happy-path polish

Test the transitions that cross ownership boundaries: two redemptions of the same reset token, membership removal between authentication and query execution, a session created before a password reset, a stale consent version, an invitation redeemed after cancellation, and a user with valid access to one workspace requesting another. Run concurrency tests against the real transaction semantics. A unit test with a map cannot prove that two database workers will not consume the same token.

For deployment, add the schema constraints before enabling the new path, make audit writes part of the security transition where loss would make the action unverifiable, and provide a reconciliation job that compares completed resets with their audit records. Dashboards should distinguish invalid recovery attempts from internal dependency failures without exposing account existence. Alert thresholds need a baseline; a raw count of failures will mostly describe traffic growth.

Finally, rehearse the audit question: given a request ID, can an authorized reviewer establish which identity authenticated, which session was active, which workspace membership and consent version were evaluated, what decision was made, and when revocation became effective? If the answer depends on reconstructing mutable rows from their current values, the system does not yet survive audit. Preserve the decision evidence, minimize sensitive data inside it, and verify retention against the policy that governs the analytics workspace.

Sources

Top comments (0)