JuliusBrussee/caveman · error

postgres: runtime current_user

Error message

postgres: runtime current_user %q differs from session_user %q

What it means

For a runtime pool, the authenticated identity (session_user) and the effective identity (current_user) must be identical. If they differ, something executed SET ROLE, which could later be undone with SET ROLE NONE to regain elevated privileges — so the library fails the validation.

Solutions

  1. Connect directly as the intended low-privilege role: put that role in the database URL/user field and remove any `options=-c%20role=...` from the connection string.
  2. Remove any server-side login triggers or role settings that execute SET ROLE on connection.
  3. If using PgBouncer, ensure the frontend and backend usernames match (no user emulation) or bypass the proxy for runtime pools.
  4. Verify with `SELECT current_user, session_user;` using the same connection parameters.

Example fix

// before
url := "postgres://admin:pw@db/app?options=-c%20role=tenant_role"
// after
url := "postgres://tenant_role:pw@db/app" // direct login as the role
Defensive patterns

Strategy: validation

Validate before calling

var cu, su string
err := pool.QueryRow(ctx, "SELECT current_user, session_user").Scan(&cu, &su)
// require cu == su before building runtime pools

Try / catch

if err := postgresconfig.ValidateRuntimeIdentity(ctx, pool, role); err != nil {
	if strings.Contains(err.Error(), "differs from session_user") {
		log.Fatal("remove SET ROLE/options from the runtime connection string")
	}
	return err
}

Prevention

When it happens

Trigger: ValidateRuntimeIdentity (via NewRuntimePool) connects as role A but current_user reports role B — typically because the connection string includes `options=-c role=...`, a search_path/SET ROLE hook, a pooled proxy (pgbouncer) that re-authenticates differently, or a login trigger issuing SET ROLE.

Common situations: Reusing a pool URL that was hardened for an admin role with SET ROLE semantics; connecting through PgBouncer configured with a different user; database-side `ALTER ROLE ... SET role` behavior or startup scripts switching roles.

Understand the failure class

Background: "You do not have permission" / 403 Forbidden errors: authenticated but not allowed — causes and fixes across open-source libraries — this error's family across 31 libraries.

Related errors


AI-assisted analysis of JuliusBrussee/caveman@3ee70a1026 (2026-09-20). Data as JSON: /api/errors/184d93f14fe6dac3. Report an issue: GitHub.

Appendix: source

Thrown at shared/platform/postgresconfig/postgresconfig.go:176

		         WHERE n.nspname='public'
		           AND c.relkind IN ('r','p')
		           AND pg_get_userbyid(c.relowner)=current_user
		       )
		FROM pg_roles r
		WHERE r.rolname=current_user
	`, expectedRole).Scan(&currentUser, &sessionUser, &superuser, &bypassRLS, &member, &ownsTenantTable)
	if err != nil {
		return fmt.Errorf("postgres: inspect runtime identity: %w", err)
	}
	return validateRuntimeIdentity(currentUser, sessionUser, expectedRole, superuser, bypassRLS, member, ownsTenantTable)
}

func validateRuntimeIdentity(currentUser, sessionUser, expectedRole string, superuser, bypassRLS, member, ownsTenantTable bool) error {
	// A safe SET ROLE is not a safe login: session_user may execute SET ROLE
	// NONE later and recover its original privileges. Runtime pools therefore
	// require the authenticated identity and effective identity to be identical.
	if currentUser != sessionUser {
		return fmt.Errorf("postgres: runtime current_user %q differs from session_user %q", currentUser, sessionUser)
	}
	if superuser || bypassRLS || ownsTenantTable {
		return fmt.Errorf("postgres: unsafe runtime identity %q (session_user=%q superuser=%t bypassrls=%t owns_tenant_table=%t)", currentUser, sessionUser, superuser, bypassRLS, ownsTenantTable)
	}
	if !member {
		return fmt.Errorf("postgres: runtime identity %q is not a member of %q", currentUser, expectedRole)
	}
	return nil
}

type tenantTableSchema struct {
	name                   string
	nullableOrganizationID bool
	rowSecurity            bool
	forceRowSecurity       bool
	policyExists           bool
	policyPermissive       bool
	policyCommand          string

View on GitHub (pinned to 3ee70a1026)