196 lines
4.9 KiB
Go
196 lines
4.9 KiB
Go
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)
|
||
}
|