{"record":{"id":"d449f1ca6799cfba","repo":"JuliusBrussee/caveman","slug":"postgres-runtime-identity-q-is-not-a-member-of","errorCode":null,"errorMessage":"postgres: runtime identity %q is not a member of %q","messagePattern":"postgres: runtime identity %q is not a member of %q","errorType":"error_code","errorClass":null,"httpStatus":null,"severity":"error","filePath":"shared/platform/postgresconfig/postgresconfig.go","lineNumber":182,"sourceCode":"\t`, expectedRole).Scan(&currentUser, &sessionUser, &superuser, &bypassRLS, &member, &ownsTenantTable)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"postgres: inspect runtime identity: %w\", err)\n\t}\n\treturn validateRuntimeIdentity(currentUser, sessionUser, expectedRole, superuser, bypassRLS, member, ownsTenantTable)\n}\n\nfunc validateRuntimeIdentity(currentUser, sessionUser, expectedRole string, superuser, bypassRLS, member, ownsTenantTable bool) error {\n\t// A safe SET ROLE is not a safe login: session_user may execute SET ROLE\n\t// NONE later and recover its original privileges. Runtime pools therefore\n\t// require the authenticated identity and effective identity to be identical.\n\tif currentUser != sessionUser {\n\t\treturn fmt.Errorf(\"postgres: runtime current_user %q differs from session_user %q\", currentUser, sessionUser)\n\t}\n\tif superuser || bypassRLS || ownsTenantTable {\n\t\treturn fmt.Errorf(\"postgres: unsafe runtime identity %q (session_user=%q superuser=%t bypassrls=%t owns_tenant_table=%t)\", currentUser, sessionUser, superuser, bypassRLS, ownsTenantTable)\n\t}\n\tif !member {\n\t\treturn fmt.Errorf(\"postgres: runtime identity %q is not a member of %q\", currentUser, expectedRole)\n\t}\n\treturn nil\n}\n\ntype tenantTableSchema struct {\n\tname                   string\n\tnullableOrganizationID bool\n\trowSecurity            bool\n\tforceRowSecurity       bool\n\tpolicyExists           bool\n\tpolicyPermissive       bool\n\tpolicyCommand          string\n\tpolicyAppliesToPublic  bool\n\tpolicyUsingExpression  string\n\tpolicyCheckExpression  string\n}\n\ntype tenantForeignKeySchema struct {","sourceCodeStart":164,"sourceCodeEnd":200,"githubUrl":"https://github.com/JuliusBrussee/caveman/blob/3ee70a102609e550bd2e68004bf5990a9341c851/shared/platform/postgresconfig/postgresconfig.go#L164-L200","documentation":"After confirming the identity is safe (not superuser, no BYPASSRLS, non-owner), the library checks that current_user is a member of the expected role passed to ValidateRuntimeIdentity. Membership is what authorizes SET ROLE into the tenant role; without it RLS policies keyed to that role cannot be assumed.","triggerScenarios":"ValidateRuntimeIdentity (via NewRuntimePool) is called with expectedRole R, but the connected role is not (directly or transitively) a member of R per pg_auth_members.","commonSituations":"Typo in the expected role name; role hierarchy not provisioned (missing GRANT app_runtime TO target_role); connecting to a different database/cluster where the membership grant was never applied; staging credentials pointed at by production config.","solutions":["Grant membership: GRANT <expected_role> TO <login_role>; as a superuser or the role's admin.","Verify the exact role name passed to ValidateRuntimeIdentity matches the role created in the database (check for case/whitespace).","Confirm membership with SELECT r.rolname FROM pg_auth_members m JOIN pg_roles r ON r.oid=m.member JOIN pg_roles t ON t.oid=m.roleid WHERE t.rolname='<expected_role>';","Ensure the same migration that creates the role also grants the membership in every environment."],"exampleFix":"-- before\n-- role app_user created, expectedRole = tenant_role, no grant\n-- after\nGRANT tenant_role TO app_user;","handlingStrategy":"validation","validationCode":"var member bool\nerr := pool.QueryRow(ctx, `\n\tSELECT EXISTS (\n\t\tSELECT 1 FROM pg_auth_members m\n\t\tJOIN pg_roles cur ON cur.oid = m.member\n\t\tJOIN pg_roles tgt ON tgt.oid = m.roleid\n\t\tWHERE cur.rolname = current_user AND tgt.rolname = $1)`, role).Scan(&member)","typeGuard":null,"tryCatchPattern":"if err := postgresconfig.ValidateRuntimeIdentity(ctx, pool, role); err != nil {\n\tif strings.Contains(err.Error(), \"is not a member of\") {\n\t\treturn fmt.Errorf(\"run GRANT %s TO <login_role>; %w\", role, err)\n\t}\n\treturn err\n}","preventionTips":["Include role creation AND membership grants in the same migration.","Compare role names case-sensitively; Postgres role names are lowercase by default.","Smoke-test role membership in CI against every target environment."],"tags":["postgres","security","roles","rls"],"backgroundTag":"insufficient-permissions","analyzedSha":"3ee70a102609e550bd2e68004bf5990a9341c851","analyzedAt":"2026-09-20T15:53:39.229Z","contentChangedAt":"2026-09-20T15:53:39.229Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}