fix(identity): 支持平台用户显式登录 AI Gateway

修复多租户身份配置将所有人类登录都强制解释为租户上下文的问题。Web 现在提供平台与租户两个受控入口,API 严格绑定 context_type、tid、issuer、application 和 subject,并为平台用户建立独立本地投影与可刷新会话。\n\n风险:新增会话身份列保持旧会话可读,新会话一律使用严格约束;未改变租户数据隔离和 API Key 行为。\n\n验证:Go 全量测试与 go vet 通过;PostgreSQL 平台投影、会话和安全事件集成测试通过;前端 lint、141 项测试与生产 build 通过;OpenAPI 生成和迁移安全测试通过。
This commit is contained in:
2026-07-31 14:24:40 +08:00
parent 88c971564a
commit c0296dbf06
29 changed files with 1061 additions and 84 deletions
@@ -0,0 +1,247 @@
package store
import (
"context"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"strings"
"github.com/easyai/easyai-ai-gateway/apps/api/internal/auth"
"github.com/google/uuid"
"github.com/jackc/pgx/v5"
)
const oidcPlatformTenantKey = "default"
func (s *Store) resolveOrProvisionOIDCPlatformUser(
ctx context.Context,
input ResolveOrProvisionOIDCUserInput,
) (ResolveOrProvisionOIDCUserResult, error) {
if input.Issuer == "" || input.Subject == "" ||
input.ContextType != "platform" ||
input.TenantMode != "multi_tenant" ||
uuid.Validate(input.ApplicationID) != nil ||
input.TenantID != "" {
return ResolveOrProvisionOIDCUserResult{},
errors.New("invalid platform OIDC user projection input")
}
tx, err := s.pool.Begin(ctx)
if err != nil {
return ResolveOrProvisionOIDCUserResult{}, err
}
defer rollbackTransaction(tx)
userKey := deriveOIDCPlatformUserKey(
input.Issuer,
input.ApplicationID,
input.Subject,
)
user, groupKey, err := loadOIDCPlatformUser(ctx, tx, userKey)
created := false
if errors.Is(err, pgx.ErrNoRows) {
if !input.ProvisioningEnabled {
return ResolveOrProvisionOIDCUserResult{},
ErrOIDCUserNotProvisioned
}
user, groupKey, created, err = s.createOIDCPlatformUser(
ctx,
tx,
userKey,
input,
)
}
if err != nil {
return ResolveOrProvisionOIDCUserResult{}, err
}
if user.Status != "active" {
return ResolveOrProvisionOIDCUserResult{}, ErrOIDCUserDisabled
}
rolesJSON, err := json.Marshal(input.Roles)
if err != nil {
return ResolveOrProvisionOIDCUserResult{}, err
}
user, err = scanUser(tx.QueryRow(ctx, `UPDATE gateway_users SET
username=COALESCE(NULLIF($2,''),username),roles=$3::jsonb,
last_login_at=now(),synced_at=now(),source_updated_at=now(),
updated_at=now()
WHERE id=$1::uuid AND source='oidc_v2_platform'
AND status='active' AND deleted_at IS NULL
RETURNING `+userColumns,
user.ID,
input.Username,
string(rolesJSON),
))
if err != nil {
if errors.Is(err, pgx.ErrNoRows) {
return ResolveOrProvisionOIDCUserResult{}, ErrOIDCUserDisabled
}
return ResolveOrProvisionOIDCUserResult{}, err
}
var auditID string
if created {
subjectHash := sha256.Sum256([]byte(input.Subject))
audit, auditErr := s.RecordAuditLogTx(ctx, tx, AuditLogInput{
Category: "identity",
Action: "identity.oidc_platform_user.provisioned",
ActorGatewayUserID: user.ID,
ActorUsername: user.Username,
ActorSource: "oidc",
ActorRoles: user.Roles,
TargetType: "gateway_user",
TargetID: user.ID,
TargetGatewayUserID: user.ID,
TargetGatewayTenantID: user.GatewayTenantID,
RequestIP: input.RequestIP,
UserAgent: input.UserAgent,
AfterState: map[string]any{
"source": "oidc_v2_platform",
"contextType": "platform",
"tenantKey": user.TenantKey,
"userGroupId": user.DefaultUserGroupID,
},
Metadata: map[string]any{
"provisioningMode": "oidc-platform-jit",
"externalSubjectHash": hex.EncodeToString(subjectHash[:])[:16],
},
})
if auditErr != nil {
return ResolveOrProvisionOIDCUserResult{}, auditErr
}
auditID = audit.ID
}
if err := tx.Commit(ctx); err != nil {
return ResolveOrProvisionOIDCUserResult{}, err
}
return ResolveOrProvisionOIDCUserResult{
User: platformAuthUser(user, groupKey, input),
Created: created,
AuditID: auditID,
}, nil
}
func loadOIDCPlatformUser(
ctx context.Context,
tx pgx.Tx,
userKey string,
) (GatewayUser, string, error) {
var userID string
if err := tx.QueryRow(ctx, `SELECT id::text
FROM gateway_users
WHERE user_key=$1 AND source='oidc_v2_platform'
FOR UPDATE`, userKey).Scan(&userID); err != nil {
return GatewayUser{}, "", err
}
user, err := scanUser(tx.QueryRow(ctx, `SELECT `+userColumns+`
FROM gateway_users WHERE id=$1::uuid`, userID))
if err != nil {
return GatewayUser{}, "", err
}
var groupKey string
if err := tx.QueryRow(ctx, `SELECT group_record.group_key
FROM gateway_users user_record
JOIN gateway_tenants tenant
ON tenant.id=user_record.gateway_tenant_id
AND tenant.status='active' AND tenant.deleted_at IS NULL
JOIN gateway_user_groups group_record
ON group_record.id=user_record.default_user_group_id
AND group_record.status='active'
WHERE user_record.id=$1::uuid`, userID).Scan(&groupKey); err != nil {
if errors.Is(err, pgx.ErrNoRows) {
return GatewayUser{}, "", ErrOIDCTenantUnavailable
}
return GatewayUser{}, "", err
}
return user, groupKey, nil
}
func (s *Store) createOIDCPlatformUser(
ctx context.Context,
tx pgx.Tx,
userKey string,
input ResolveOrProvisionOIDCUserInput,
) (GatewayUser, string, bool, error) {
gatewayTenantID, groupID, groupKey, err :=
loadOIDCProvisioningTenant(ctx, tx, oidcPlatformTenantKey)
if err != nil {
if errors.Is(err, pgx.ErrNoRows) {
return GatewayUser{}, "", false, ErrOIDCTenantUnavailable
}
return GatewayUser{}, "", false, err
}
rolesJSON, err := json.Marshal(input.Roles)
if err != nil {
return GatewayUser{}, "", false, err
}
metadataJSON, err := json.Marshal(map[string]any{
"provisioningMode": "oidc-platform-jit",
"contextType": "platform",
"applicationId": input.ApplicationID,
})
if err != nil {
return GatewayUser{}, "", false, err
}
username := input.Username
if username == "" {
username = "oidc-platform-" +
strings.TrimPrefix(userKey, "oidc2-platform:")[:12]
}
user, err := scanUser(tx.QueryRow(ctx, `INSERT INTO gateway_users(
user_key,source,external_user_id,username,gateway_tenant_id,
tenant_id,tenant_key,default_user_group_id,roles,auth_profile,
metadata,status,last_login_at,synced_at,source_updated_at
) VALUES(
$1,'oidc_v2_platform',NULL,$2,$3::uuid,NULL,$4,$5::uuid,
$6::jsonb,'{}'::jsonb,$7::jsonb,'active',now(),now(),now()
)
ON CONFLICT(user_key) DO NOTHING
RETURNING `+userColumns,
userKey,
username,
gatewayTenantID,
oidcPlatformTenantKey,
groupID,
string(rolesJSON),
string(metadataJSON),
))
if errors.Is(err, pgx.ErrNoRows) {
user, groupKey, err = loadOIDCPlatformUser(ctx, tx, userKey)
return user, groupKey, false, err
}
if err != nil {
return GatewayUser{}, "", false, err
}
if _, err := s.ensureWalletAccount(ctx, tx, user.ID, "resource"); err != nil {
return GatewayUser{}, "", false, err
}
return user, groupKey, true, nil
}
func deriveOIDCPlatformUserKey(
issuer string,
applicationID string,
subject string,
) string {
sum := sha256.Sum256([]byte(
strings.TrimRight(issuer, "/") + "\x00" +
applicationID + "\x00" + subject,
))
return fmt.Sprintf("oidc2-platform:%x", sum)
}
func platformAuthUser(
user GatewayUser,
groupKey string,
input ResolveOrProvisionOIDCUserInput,
) *auth.User {
result := authUserFromOIDCProjection(user, groupKey)
result.ID = input.Subject
result.ContextType = "platform"
result.TenantID = ""
result.Issuer = input.Issuer
result.ApplicationID = input.ApplicationID
result.OIDCClientID = input.OIDCClientID
return result
}
@@ -0,0 +1,60 @@
package store
import "testing"
func TestOIDCProjectionKindRequiresExplicitCompatibleContext(t *testing.T) {
applicationID := "11111111-1111-4111-8111-111111111111"
tests := []struct {
name string
input ResolveOrProvisionOIDCUserInput
want string
}{
{
name: "platform in multi-tenant application",
input: ResolveOrProvisionOIDCUserInput{
ContextType: "platform", TenantMode: "multi_tenant",
ApplicationID: applicationID,
},
want: "platform",
},
{
name: "tenant in multi-tenant application",
input: ResolveOrProvisionOIDCUserInput{
ContextType: "tenant", TenantMode: "multi_tenant",
ApplicationID: applicationID,
TenantID: "22222222-2222-4222-8222-222222222222",
},
want: "multi_tenant",
},
{
name: "tenant in single-tenant application",
input: ResolveOrProvisionOIDCUserInput{
ContextType: "tenant", TenantMode: "single_tenant",
TenantID: "tenant-contract-id",
},
want: "single_tenant",
},
{
name: "missing context",
input: ResolveOrProvisionOIDCUserInput{
TenantMode: "multi_tenant", ApplicationID: applicationID,
TenantID: "22222222-2222-4222-8222-222222222222",
},
},
{
name: "platform context with tenant",
input: ResolveOrProvisionOIDCUserInput{
ContextType: "platform", TenantMode: "multi_tenant",
ApplicationID: applicationID,
TenantID: "22222222-2222-4222-8222-222222222222",
},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
if got := oidcProjectionKind(test.input); got != test.want {
t.Fatalf("projection kind=%q, want %q", got, test.want)
}
})
}
}
+44 -13
View File
@@ -19,6 +19,7 @@ type OIDCSession struct {
ExternalUserID string
Issuer string
ApplicationID string
ContextType string
TenantID string
OIDCClientID string
UserStatus string
@@ -43,6 +44,8 @@ type CreateOIDCSessionInput struct {
OIDCClientID string
Issuer string
ApplicationID string
ContextType string
Subject string
TenantID string
TokenCiphertext []byte
AccessTokenExpiresAt time.Time
@@ -57,26 +60,51 @@ func (s *Store) CreateOIDCSession(ctx context.Context, input CreateOIDCSessionIn
INSERT INTO gateway_oidc_sessions (
session_token_hash, gateway_user_id, gateway_tenant_id, token_ciphertext,
access_token_expires_at, last_seen_at, idle_expires_at, absolute_expires_at,
oidc_user_binding_id, oidc_client_id
oidc_user_binding_id, oidc_client_id, issuer, application_id,
context_type, subject, tenant_id
)
SELECT $1, u.id, $3::uuid, $4, $5, $6, $7, $8, NULLIF($9, '')::uuid, NULLIF($10, '')
SELECT $1, u.id, $3::uuid, $4, $5, $6, $7, $8,
NULLIF($9, '')::uuid, NULLIF($10, ''), $11, $12, $13, $14,
NULLIF($15, '')
FROM gateway_users u
LEFT JOIN gateway_oidc_user_bindings ub ON ub.id = NULLIF($9, '')::uuid
LEFT JOIN gateway_oidc_tenant_bindings tb ON tb.id = ub.tenant_binding_id
WHERE u.id = $2::uuid
AND u.gateway_tenant_id = $3::uuid
AND ($9 = '' OR (
ub.gateway_user_id = u.id
AND tb.gateway_tenant_id = u.gateway_tenant_id
AND tb.issuer = $11
AND tb.application_id = $12
AND tb.external_tenant_id = $13
))
AND (
(
$13 = 'platform'
AND $9 = ''
AND $15 = ''
AND u.source = 'oidc_v2_platform'
)
OR
(
$13 = 'tenant'
AND (
(
$9 = ''
AND u.source = 'oidc'
AND u.external_user_id = $14
)
OR
(
ub.gateway_user_id = u.id
AND ub.subject = $14
AND tb.gateway_tenant_id = u.gateway_tenant_id
AND tb.issuer = $11
AND tb.application_id = $12
AND tb.external_tenant_id = $15
)
)
)
)
RETURNING id::text`,
input.SessionTokenHash, input.GatewayUserID, input.GatewayTenantID, input.TokenCiphertext,
input.AccessTokenExpiresAt, input.LastSeenAt, input.IdleExpiresAt, input.AbsoluteExpiresAt,
input.OIDCUserBindingID, input.OIDCClientID,
input.Issuer, input.ApplicationID, input.TenantID,
input.Issuer, input.ApplicationID, input.ContextType,
input.Subject, input.TenantID,
).Scan(&id)
if err != nil {
return OIDCSession{}, err
@@ -165,8 +193,10 @@ WHERE idle_expires_at <= $1 OR absolute_expires_at <= $1`, now)
const oidcSessionColumns = `
s.id::text, s.session_token_hash, s.gateway_user_id::text, s.gateway_tenant_id::text,
COALESCE(s.oidc_user_binding_id::text, ''), COALESCE(ub.subject, u.external_user_id, ''),
COALESCE(tb.issuer, ''), COALESCE(tb.application_id, ''), COALESCE(tb.external_tenant_id, ''),
COALESCE(s.oidc_user_binding_id::text, ''),
COALESCE(s.subject, ub.subject, u.external_user_id, ''),
COALESCE(s.issuer, tb.issuer, ''), COALESCE(s.application_id, tb.application_id, ''),
COALESCE(s.context_type, ''), COALESCE(s.tenant_id, tb.external_tenant_id, ''),
COALESCE(s.oidc_client_id, ''), u.status, u.deleted_at IS NOT NULL,
s.token_ciphertext, s.access_token_expires_at, s.last_seen_at, s.idle_expires_at,
s.absolute_expires_at, s.refresh_version, COALESCE(s.refresh_lock_id::text, ''),
@@ -177,7 +207,8 @@ func scanOIDCSession(row pgx.Row) (OIDCSession, error) {
err := row.Scan(
&item.ID, &item.SessionTokenHash, &item.GatewayUserID, &item.GatewayTenantID,
&item.OIDCUserBindingID, &item.ExternalUserID, &item.Issuer, &item.ApplicationID,
&item.TenantID, &item.OIDCClientID, &item.UserStatus, &item.UserDeleted, &item.TokenCiphertext,
&item.ContextType, &item.TenantID, &item.OIDCClientID,
&item.UserStatus, &item.UserDeleted, &item.TokenCiphertext,
&item.AccessTokenExpiresAt, &item.LastSeenAt, &item.IdleExpiresAt, &item.AbsoluteExpiresAt,
&item.RefreshVersion, &item.RefreshLockID, &item.RefreshLockUntil, &item.CreatedAt, &item.UpdatedAt,
)
+38 -1
View File
@@ -11,6 +11,7 @@ import (
"time"
"github.com/easyai/easyai-ai-gateway/apps/api/internal/auth"
"github.com/google/uuid"
"github.com/jackc/pgx/v5"
)
@@ -26,6 +27,7 @@ type ResolveOrProvisionOIDCUserInput struct {
Subject string
Username string
Roles []string
ContextType string
TenantID string
TenantMode string
TenantName string
@@ -58,8 +60,16 @@ type oidcUserProjection struct {
func (s *Store) ResolveOrProvisionOIDCUser(ctx context.Context, input ResolveOrProvisionOIDCUserInput) (ResolveOrProvisionOIDCUserResult, error) {
input = normalizeOIDCUserInput(input)
if input.TenantMode == "multi_tenant" {
switch oidcProjectionKind(input) {
case "platform":
return s.resolveOrProvisionOIDCPlatformUser(ctx, input)
case "multi_tenant":
return s.resolveOrProvisionOIDCMultiTenantUser(ctx, input)
case "single_tenant":
// Continue through the fixed local Tenant projection below.
default:
return ResolveOrProvisionOIDCUserResult{},
errors.New("invalid OIDC user context")
}
if input.Issuer == "" || input.Subject == "" || input.TenantID == "" {
return ResolveOrProvisionOIDCUserResult{}, errors.New("invalid OIDC user projection input")
@@ -187,6 +197,31 @@ RETURNING `+userColumns,
}, nil
}
func oidcProjectionKind(input ResolveOrProvisionOIDCUserInput) string {
switch input.ContextType {
case "platform":
if input.TenantMode == "multi_tenant" &&
uuid.Validate(input.ApplicationID) == nil &&
input.TenantID == "" {
return "platform"
}
case "tenant":
if input.TenantID == "" {
return ""
}
switch input.TenantMode {
case "multi_tenant":
if uuid.Validate(input.ApplicationID) == nil &&
uuid.Validate(input.TenantID) == nil {
return "multi_tenant"
}
case "single_tenant":
return "single_tenant"
}
}
return ""
}
func (s *Store) syncExistingOIDCUser(ctx context.Context, tx pgx.Tx, projection oidcUserProjection, input ResolveOrProvisionOIDCUserInput) (ResolveOrProvisionOIDCUserResult, error) {
if projection.userDeleted || projection.user.Status != "active" {
return ResolveOrProvisionOIDCUserResult{}, ErrOIDCUserDisabled
@@ -308,6 +343,7 @@ func normalizeOIDCUserInput(input ResolveOrProvisionOIDCUserInput) ResolveOrProv
input.Issuer = strings.TrimRight(strings.TrimSpace(input.Issuer), "/")
input.Subject = strings.TrimSpace(input.Subject)
input.Username = strings.TrimSpace(input.Username)
input.ContextType = strings.TrimSpace(input.ContextType)
input.TenantID = strings.TrimSpace(input.TenantID)
input.ApplicationID = strings.TrimSpace(input.ApplicationID)
input.TenantMode = strings.TrimSpace(input.TenantMode)
@@ -355,6 +391,7 @@ func authUserFromOIDCProjection(user GatewayUser, userGroupKey string) *auth.Use
ID: user.ExternalUserID,
Username: user.Username,
Roles: user.Roles,
ContextType: "tenant",
TenantID: user.TenantID,
GatewayTenantID: user.GatewayTenantID,
TenantKey: user.TenantKey,
@@ -2,8 +2,10 @@ package store
import (
"context"
"crypto/sha256"
"os"
"path/filepath"
"reflect"
"runtime"
"sort"
"strings"
@@ -35,7 +37,8 @@ func TestResolveOrProvisionOIDCMultiTenantUserIsIdempotentIsolatedAndReusable(t
input := func(tenantID, name, slug string) ResolveOrProvisionOIDCUserInput {
return ResolveOrProvisionOIDCUserInput{
Issuer: issuer, ApplicationID: applicationID, Subject: subject, Username: "shared-subject",
Roles: []string{"basic"}, TenantID: tenantID, TenantMode: "multi_tenant",
Roles: []string{"basic"}, ContextType: "tenant",
TenantID: tenantID, TenantMode: "multi_tenant",
TenantName: name, TenantSlug: slug, TenantMetadataStatus: "synced",
TenantMetadataVersion: "v1", TenantMetadataETag: `"v1"`,
TenantMetadataUpdatedAt: time.Unix(1_780_000_000, 0).UTC(),
@@ -173,7 +176,9 @@ func TestResolveOrProvisionOIDCUserLifecycleAndConcurrency(t *testing.T) {
Subject: subject,
Username: "jit-user-" + suffix,
Roles: []string{"basic"},
ContextType: "tenant",
TenantID: "auth-center-test-tenant",
TenantMode: "single_tenant",
GatewayTenantKey: "default",
ProvisioningEnabled: true,
}
@@ -299,6 +304,120 @@ FROM gateway_users WHERE id = $1::uuid`, firstID).Scan(&displayName, &email, &ma
}
}
func TestResolveOrProvisionOIDCPlatformUserUsesExplicitContextWithoutExternalTenant(t *testing.T) {
databaseURL := strings.TrimSpace(os.Getenv("AI_GATEWAY_TEST_DATABASE_URL"))
if databaseURL == "" {
t.Skip("set AI_GATEWAY_TEST_DATABASE_URL to run OIDC JIT PostgreSQL integration tests")
}
ctx := context.Background()
applyOIDCJITTestMigrations(t, ctx, databaseURL)
db, err := Connect(ctx, databaseURL)
if err != nil {
t.Fatalf("connect store: %v", err)
}
defer db.Close()
suffix := time.Now().UTC().Format("20060102150405.000000000")
subject := "platform-context-" + suffix
applicationID := uuid.NewString()
t.Cleanup(func() {
_, _ = db.pool.Exec(context.Background(), `
DELETE FROM gateway_audit_logs
WHERE target_gateway_user_id IN (
SELECT id FROM gateway_users
WHERE source='oidc_v2_platform'
AND user_key=$1
)`, deriveOIDCPlatformUserKey(
"https://auth.test.example/issuer/shared",
applicationID,
subject,
))
_, _ = db.pool.Exec(context.Background(), `
DELETE FROM gateway_users
WHERE source='oidc_v2_platform'
AND user_key=$1`, deriveOIDCPlatformUserKey(
"https://auth.test.example/issuer/shared",
applicationID,
subject,
))
})
result, err := db.ResolveOrProvisionOIDCUser(
ctx,
ResolveOrProvisionOIDCUserInput{
Issuer: "https://auth.test.example/issuer/shared",
ApplicationID: applicationID,
Subject: subject,
Username: "platform-user-" + suffix,
Roles: []string{"admin"},
ContextType: "platform",
TenantMode: "multi_tenant",
ProvisioningEnabled: true,
},
)
if err != nil {
t.Fatalf("resolve platform user: %v", err)
}
if !result.Created || result.User == nil ||
result.User.ID != subject ||
result.User.ContextType != "platform" ||
result.User.TenantID != "" ||
result.User.TenantKey != oidcPlatformTenantKey ||
result.User.GatewayTenantID == "" ||
result.AuditID == "" {
t.Fatalf("platform projection=%#v", result)
}
now := time.Now().UTC()
sessionHash := sha256.Sum256([]byte("platform-session-" + suffix))
session, err := db.CreateOIDCSession(ctx, CreateOIDCSessionInput{
SessionTokenHash: sessionHash[:],
GatewayUserID: result.User.GatewayUserID,
GatewayTenantID: result.User.GatewayTenantID,
OIDCClientID: "gateway-browser",
Issuer: result.User.Issuer,
ApplicationID: applicationID,
ContextType: "platform",
Subject: subject,
TokenCiphertext: []byte("encrypted-test-token"),
AccessTokenExpiresAt: now.Add(time.Hour),
LastSeenAt: now,
IdleExpiresAt: now.Add(time.Hour),
AbsoluteExpiresAt: now.Add(2 * time.Hour),
})
if err != nil {
t.Fatalf("create platform session: %v", err)
}
if session.ContextType != "platform" ||
session.ExternalUserID != subject ||
session.Issuer != result.User.Issuer ||
session.ApplicationID != applicationID ||
session.TenantID != "" ||
session.OIDCUserBindingID != "" {
t.Fatalf("platform session=%#v", session)
}
repeated, err := db.ResolveOrProvisionOIDCUser(
ctx,
ResolveOrProvisionOIDCUserInput{
Issuer: "https://auth.test.example/issuer/shared",
ApplicationID: applicationID,
Subject: subject,
Username: "platform-user-renamed-" + suffix,
Roles: []string{"viewer"},
ContextType: "platform",
TenantMode: "multi_tenant",
ProvisioningEnabled: false,
},
)
if err != nil || repeated.Created ||
repeated.User.GatewayUserID != result.User.GatewayUserID ||
!reflect.DeepEqual(repeated.User.Roles, []string{"viewer"}) {
t.Fatalf("repeated platform projection=%#v err=%v", repeated, err)
}
}
func TestResolveOrProvisionOIDCUserRejectsMissingMappingWithoutWrites(t *testing.T) {
databaseURL := strings.TrimSpace(os.Getenv("AI_GATEWAY_TEST_DATABASE_URL"))
if databaseURL == "" {
@@ -318,7 +437,9 @@ func TestResolveOrProvisionOIDCUserRejectsMissingMappingWithoutWrites(t *testing
Subject: subject,
Username: "missing-user",
Roles: []string{"basic"},
ContextType: "tenant",
TenantID: "auth-center-test-tenant",
TenantMode: "single_tenant",
GatewayTenantKey: "missing-tenant-key",
}
@@ -204,7 +204,8 @@ func TestApplicationScopedSecurityEventsRevokeOnlyMatchingTenantSessions(t *test
projection := func(tenantID string) *auth.User {
result, err := db.ResolveOrProvisionOIDCUser(ctx, ResolveOrProvisionOIDCUserInput{
Issuer: subjectIssuer, ApplicationID: applicationID, Subject: subject, Username: "shared-user",
Roles: []string{"basic"}, TenantID: tenantID, TenantMode: "multi_tenant",
Roles: []string{"basic"}, ContextType: "tenant",
TenantID: tenantID, TenantMode: "multi_tenant",
TenantName: "Tenant " + tenantID[:8], TenantSlug: "tenant-" + tenantID[:8],
TenantMetadataStatus: "synced", TenantMetadataVersion: "1",
TenantMetadataUpdatedAt: time.Now().UTC(), OIDCClientID: "gateway-browser", ProvisioningEnabled: true,
@@ -225,6 +226,7 @@ func TestApplicationScopedSecurityEventsRevokeOnlyMatchingTenantSessions(t *test
SessionTokenHash: sessionHash[:], GatewayUserID: user.GatewayUserID,
GatewayTenantID: user.GatewayTenantID, OIDCUserBindingID: user.OIDCUserBindingID,
OIDCClientID: "gateway-browser", Issuer: subjectIssuer, ApplicationID: applicationID,
ContextType: "tenant", Subject: user.ID,
TenantID: user.TenantID, TokenCiphertext: []byte{marker},
AccessTokenExpiresAt: now.Add(time.Hour), LastSeenAt: now,
IdleExpiresAt: now.Add(time.Hour), AbsoluteExpiresAt: now.Add(2 * time.Hour),