package tenantperm import ( "context" "database/sql" "sync" "aijianzhan/platform/internal/authx" ) // Store 公司权限额度:超管授予,公司内分配不得越界。 // configured=false 表示从未配置 → 读时视为全量默认;configured=true 时允许空额度。 type Store interface { Get(ctx context.Context, tenantID int64) ([]string, error) Set(ctx context.Context, tenantID int64, perms []string) error Allows(ctx context.Context, tenantID int64, perm string) (bool, error) EnsureDefault(ctx context.Context, tenantID int64) error Configured(ctx context.Context, tenantID int64) (bool, error) } type MemoryStore struct { mu sync.Mutex by map[int64][]string // key 存在即已配置 } func NewMemoryStore() *MemoryStore { return &MemoryStore{by: map[int64][]string{}} } func (s *MemoryStore) Configured(_ context.Context, tenantID int64) (bool, error) { s.mu.Lock() defer s.mu.Unlock() _, ok := s.by[tenantID] return ok, nil } func (s *MemoryStore) Get(_ context.Context, tenantID int64) ([]string, error) { s.mu.Lock() defer s.mu.Unlock() if ps, ok := s.by[tenantID]; ok { return append([]string{}, ps...), nil } return authx.CompanyPermCatalog(), nil } func (s *MemoryStore) Set(_ context.Context, tenantID int64, perms []string) error { s.mu.Lock() defer s.mu.Unlock() s.by[tenantID] = intersectCatalog(perms) // 可为空切片 return nil } func (s *MemoryStore) Allows(ctx context.Context, tenantID int64, perm string) (bool, error) { ps, err := s.Get(ctx, tenantID) if err != nil { return false, err } want := authx.NormalizePerm(perm) for _, p := range ps { if p == want { return true, nil } } return false, nil } func (s *MemoryStore) EnsureDefault(ctx context.Context, tenantID int64) error { s.mu.Lock() defer s.mu.Unlock() if _, ok := s.by[tenantID]; ok { return nil } s.by[tenantID] = authx.CompanyPermCatalog() return nil } type PostgresStore struct{ DB *sql.DB } func NewPostgresStore(db *sql.DB) *PostgresStore { return &PostgresStore{DB: db} } func (s *PostgresStore) Configured(ctx context.Context, tenantID int64) (bool, error) { var n int err := s.DB.QueryRowContext(ctx, ` SELECT COUNT(*) FROM platform_meta.tenant_entitlement_state WHERE tenant_id=$1`, tenantID).Scan(&n) return n > 0, err } func (s *PostgresStore) Get(ctx context.Context, tenantID int64) ([]string, error) { ok, err := s.Configured(ctx, tenantID) if err != nil { return nil, err } if !ok { return authx.CompanyPermCatalog(), nil } rows, err := s.DB.QueryContext(ctx, ` SELECT perm FROM platform_meta.tenant_permissions WHERE tenant_id=$1 ORDER BY perm`, tenantID) if err != nil { return nil, err } defer rows.Close() out := make([]string, 0) for rows.Next() { var p string if err := rows.Scan(&p); err != nil { return nil, err } out = append(out, authx.NormalizePerm(p)) } if err := rows.Err(); err != nil { return nil, err } return authx.NormalizePerms(out), nil } func (s *PostgresStore) Set(ctx context.Context, tenantID int64, perms []string) error { allowed := intersectCatalog(perms) tx, err := s.DB.BeginTx(ctx, nil) if err != nil { return err } defer func() { _ = tx.Rollback() }() if _, err := tx.ExecContext(ctx, `DELETE FROM platform_meta.tenant_permissions WHERE tenant_id=$1`, tenantID); err != nil { return err } for _, p := range allowed { if _, err := tx.ExecContext(ctx, ` INSERT INTO platform_meta.tenant_permissions(tenant_id, perm) VALUES($1,$2)`, tenantID, p); err != nil { return err } } if _, err := tx.ExecContext(ctx, ` INSERT INTO platform_meta.tenant_entitlement_state(tenant_id, updated_at) VALUES($1, now()) ON CONFLICT (tenant_id) DO UPDATE SET updated_at=now()`, tenantID); err != nil { return err } return tx.Commit() } func (s *PostgresStore) Allows(ctx context.Context, tenantID int64, perm string) (bool, error) { ps, err := s.Get(ctx, tenantID) if err != nil { return false, err } want := authx.NormalizePerm(perm) for _, p := range ps { if p == want { return true, nil } } return false, nil } func (s *PostgresStore) EnsureDefault(ctx context.Context, tenantID int64) error { ok, err := s.Configured(ctx, tenantID) if err != nil { return err } if ok { return nil } return s.Set(ctx, tenantID, authx.CompanyPermCatalog()) } func intersectCatalog(perms []string) []string { cat := map[string]struct{}{} for _, p := range authx.CompanyPermCatalog() { cat[p] = struct{}{} } out := make([]string, 0, len(perms)) seen := map[string]struct{}{} for _, p := range authx.NormalizePerms(perms) { if _, ok := cat[p]; !ok { continue } if _, ok := seen[p]; ok { continue } seen[p] = struct{}{} out = append(out, p) } return out } func MustAllow(ctx context.Context, st Store, tenantID int64, want []string) error { if st == nil || tenantID <= 0 { return nil } allowance, err := st.Get(ctx, tenantID) if err != nil { return err } return authx.AssertWithinEntitlement(want, allowance) }