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
187 lines
6.5 KiB
Go
187 lines
6.5 KiB
Go
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()
|
||
}
|