Files
backend/internal/service/casbin_service_test.go
lafay eb931bf1c6
All checks were successful
Build Backend / build (push) Successful in 2m19s
Build Backend / build-docker (push) Successful in 46s
feat(auth): implement session-based token management with revocation support
Add Session model, SessionService, and SessionRepository to track user login
sessions and enable token revocation on auth-critical events.

- Introduce explicit TokenType (access/refresh) in JWT claims to prevent
  refresh token misuse via access token endpoints
- Add SessionID field to JWT claims, enabling stateless JWT validation
  against revoked sessions
- Replace legacy Auth middleware with RequireAuth/OptionalAuth pipeline
  that validates token type, account status, and session validity
- Implement session revocation on password change, reset, user ban/inactive,
  and explicit logout
- Add Principal cache with active invalidation for banned/role-changed users
- Fix IDOR vulnerability: GetMessagesByCursor now validates currentUserID
  is conversation participant via GetParticipantStrict
- Add group member visibility checks: announcements, group info, member list
  now require group membership
- Simplify Casbin policy: remove g grouping, use r.sub == p.sub matcher
  with globMatch; user_roles table is single source of truth for roles
- Add migration logic to clean legacy casbin g rules and migrate old p rules
  from path-style to admin/<domain> resource naming
2026-07-05 18:28:08 +08:00

187 lines
6.5 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package service
import (
"context"
"testing"
withmodel "with_you/internal/model"
"github.com/casbin/casbin/v3"
casbinmodel "github.com/casbin/casbin/v3/model"
)
// newTestEnforcer 构造一个内存 Enforcer加载与生产相同的 model.conf 语义globMatch
// 并按 seedPermissions 的约定注入角色策略,便于在不依赖 DB 的情况下验证 matcher 行为。
func newTestEnforcer(t *testing.T) *casbin.Enforcer {
t.Helper()
// 与 configs/casbin/model.conf 保持一致;用 globMatch 而非 keyMatch2。
mText := `
[request_definition]
r = sub, obj, act
[policy_definition]
p = sub, obj, act
[role_definition]
g = _, _
[policy_effect]
e = some(where (p.eft == allow))
[matchers]
m = r.sub == p.sub && globMatch(r.obj, p.obj) && (r.act == p.act || p.act == "*")
`
m, err := casbinmodel.NewModelFromString(mText)
if err != nil {
t.Fatalf("parse model: %v", err)
}
e, err := casbin.NewEnforcer(m)
if err != nil {
t.Fatalf("new enforcer: %v", err)
}
// 与 seedPermissions 一致super_admin=admin.** 通配admin 业务能力,
// moderator 内容审核。admin.logs.export 仅 super_admin 拥有admin 只有 admin.logs read
policies := [][]string{
{withmodel.RoleSuperAdmin, "admin/**", "*"},
{withmodel.RoleAdmin, "admin/users", "read"},
{withmodel.RoleAdmin, "admin/users/status", "write"},
{withmodel.RoleAdmin, "admin/users/devices", "read"},
{withmodel.RoleAdmin, "admin/posts", "*"},
{withmodel.RoleAdmin, "admin/comments", "*"},
{withmodel.RoleAdmin, "admin/groups", "*"},
{withmodel.RoleAdmin, "admin/channels", "*"},
{withmodel.RoleAdmin, "admin/dashboard", "read"},
{withmodel.RoleAdmin, "admin/reports", "*"},
{withmodel.RoleAdmin, "admin/verifications", "*"},
{withmodel.RoleAdmin, "admin/profile_audits", "*"},
{withmodel.RoleAdmin, "admin/materials", "*"},
{withmodel.RoleAdmin, "admin/logs", "read"},
{withmodel.RoleModerator, "admin/posts", "read"},
{withmodel.RoleModerator, "admin/posts", "write"},
{withmodel.RoleModerator, "admin/comments", "read"},
{withmodel.RoleModerator, "admin/comments", "write"},
{withmodel.RoleModerator, "admin/reports", "read"},
{withmodel.RoleModerator, "admin/reports", "write"},
}
for _, p := range policies {
if _, err := e.AddPolicy(p[0], p[1], p[2]); err != nil {
t.Fatalf("add policy %v: %v", p, err)
}
}
return e
}
// mustAllow / mustDeny 简化断言。
func mustAllow(t *testing.T, e *casbin.Enforcer, sub, obj, act string) {
t.Helper()
ok, err := e.Enforce(sub, obj, act)
if err != nil {
t.Fatalf("Enforce(%s,%s,%s) err: %v", sub, obj, act, err)
}
if !ok {
t.Errorf("Enforce(%s,%s,%s) = false, want true", sub, obj, act)
}
}
func mustDeny(t *testing.T, e *casbin.Enforcer, sub, obj, act string) {
t.Helper()
ok, err := e.Enforce(sub, obj, act)
if err != nil {
t.Fatalf("Enforce(%s,%s,%s) err: %v", sub, obj, act, err)
}
if ok {
t.Errorf("Enforce(%s,%s,%s) = true, want false", sub, obj, act)
}
}
// TestSuperAdminWildcard 通配多级资源globMatch ** 跨 '.')。
func TestSuperAdminWildcard(t *testing.T) {
e := newTestEnforcer(t)
mustAllow(t, e, withmodel.RoleSuperAdmin, "admin/users/roles/write", "write")
mustAllow(t, e, withmodel.RoleSuperAdmin, "admin/logs/export", "read")
mustAllow(t, e, withmodel.RoleSuperAdmin, "admin/dashboard", "read")
}
// TestAdminBusinessCapabilities admin 拥有的业务能力放行。
func TestAdminBusinessCapabilities(t *testing.T) {
e := newTestEnforcer(t)
mustAllow(t, e, withmodel.RoleAdmin, "admin/posts", "write")
mustAllow(t, e, withmodel.RoleAdmin, "admin/posts", "read")
mustAllow(t, e, withmodel.RoleAdmin, "admin/users", "read")
mustAllow(t, e, withmodel.RoleAdmin, "admin/logs", "read")
}
// TestAdminCannotReachSuperAdminOnlyResources admin 不能触及 super_admin 独占资源。
// - admin.logs.export 仅 super_admin 拥有admin 应被拒。
// - admin.users.roles.write 仅 super_admin 拥有。
// - admin.roles.* 仅 super_admin 拥有。
func TestAdminCannotReachSuperAdminOnlyResources(t *testing.T) {
e := newTestEnforcer(t)
mustDeny(t, e, withmodel.RoleAdmin, "admin/logs/export", "read")
mustDeny(t, e, withmodel.RoleAdmin, "admin/users/roles", "write")
mustDeny(t, e, withmodel.RoleAdmin, "admin/roles", "read")
// admin.users.status.write 是 admin 自己的能力,确认不受影响。
mustAllow(t, e, withmodel.RoleAdmin, "admin/users/status", "write")
}
// TestModeratorContentModeration moderator 仅内容审核。
func TestModeratorContentModeration(t *testing.T) {
e := newTestEnforcer(t)
mustAllow(t, e, withmodel.RoleModerator, "admin/posts", "read")
mustAllow(t, e, withmodel.RoleModerator, "admin/comments", "write")
mustAllow(t, e, withmodel.RoleModerator, "admin/reports", "write")
// moderator 不应触及用户管理。
mustDeny(t, e, withmodel.RoleModerator, "admin/users", "read")
mustDeny(t, e, withmodel.RoleModerator, "admin/logs", "read")
}
// TestWildcardDoesNotCrossBoundaryWithSingleStar 校验 admin/* 在 globMatch 下不跨 '/'。
// 此用例用于防止未来有人误把 super_admin 通配写回 admin/*(单层),导致 super_admin 实际拿不到
// admin/users/roles/write 这类多级资源的能力(应使用 admin/**)。
//
// 注意:本测试构造独立的 Enforcer仅注入 admin/* 单层策略,避免 newTestEnforcer 的
// admin/** 策略干扰断言。
func TestWildcardDoesNotCrossBoundaryWithSingleStar(t *testing.T) {
mText := `
[request_definition]
r = sub, obj, act
[policy_definition]
p = sub, obj, act
[role_definition]
g = _, _
[policy_effect]
e = some(where (p.eft == allow))
[matchers]
m = r.sub == p.sub && globMatch(r.obj, p.obj) && (r.act == p.act || p.act == "*")
`
m, err := casbinmodel.NewModelFromString(mText)
if err != nil {
t.Fatalf("parse model: %v", err)
}
e, err := casbin.NewEnforcer(m)
if err != nil {
t.Fatalf("new enforcer: %v", err)
}
if _, err := e.AddPolicy(withmodel.RoleSuperAdmin, "admin/*", "*"); err != nil {
t.Fatalf("add policy: %v", err)
}
// 单层 * 不跨 '/'admin/* 仅命中 admin/<segment>,不能命中 admin/users/roles/write。
mustDeny(t, e, withmodel.RoleSuperAdmin, "admin/users/roles/write", "write")
mustAllow(t, e, withmodel.RoleSuperAdmin, "admin/posts", "write")
}
// TestEnforceForUser_NoRolesNoPanic 用户无角色时 EnforceForUser 走默认 user 角色。
func TestEnforceForUser_NoRolesNoPanic(t *testing.T) {
e := newTestEnforcer(t)
// user 角色无任何 admin 策略,应全部拒绝。
mustDeny(t, e, "user", "admin/posts", "read")
// 防止 lint 抱怨未使用 context。
_ = context.Background()
}