2026-06-22 14:31:10 +08:00
package app
import (
"context"
"database/sql"
"encoding/json"
"errors"
"fmt"
"net/http"
"sort"
"strings"
"time"
)
const (
PermissionAdminOverview = "admin.overview.view"
PermissionUsersView = "admin.users.view"
PermissionUsersCreate = "admin.users.create"
PermissionUsersUpdate = "admin.users.update"
PermissionUsersDelete = "admin.users.delete"
PermissionUsersResetPassword = "admin.users.reset_password"
PermissionGroupsView = "admin.permission_groups.view"
PermissionGroupsCreate = "admin.permission_groups.create"
PermissionGroupsUpdate = "admin.permission_groups.update"
PermissionGroupsDelete = "admin.permission_groups.delete"
PermissionDomainsView = "admin.domains.view"
PermissionDomainsCreate = "admin.domains.create"
PermissionDomainsUpdate = "admin.domains.update"
PermissionDomainsDelete = "admin.domains.delete"
PermissionDNSView = "admin.dns.view"
PermissionDNSCheck = "admin.dns.check"
PermissionMailboxesView = "admin.mailboxes.view"
PermissionMailboxesCreate = "admin.mailboxes.create"
PermissionMailboxesUpdate = "admin.mailboxes.update"
PermissionMailboxesDelete = "admin.mailboxes.delete"
PermissionAliasesView = "admin.aliases.view"
PermissionAliasesCreate = "admin.aliases.create"
PermissionAliasesUpdate = "admin.aliases.update"
PermissionAliasesDelete = "admin.aliases.delete"
PermissionMessagesView = "admin.messages.view"
PermissionMessagesRead = "admin.messages.read"
PermissionMessagesAttachment = "admin.messages.attachments"
PermissionSettingsView = "admin.settings.view"
PermissionSettingsUpdate = "admin.settings.update"
PermissionSettingsTestSMTP = "admin.settings.test_smtp"
PermissionTemplatesView = "admin.templates.view"
PermissionTemplatesUpdate = "admin.templates.update"
PermissionTemplatesReset = "admin.templates.reset"
PermissionUsersManage = PermissionUsersUpdate
PermissionGroupsManage = PermissionGroupsUpdate
PermissionDomainsManage = PermissionDomainsUpdate
PermissionDNSManage = PermissionDNSCheck
PermissionMailboxesManage = PermissionMailboxesUpdate
PermissionAliasesManage = PermissionAliasesUpdate
PermissionSystemSettings = PermissionSettingsUpdate
)
const (
PermissionGroupSuperAdmin = "pg_super_admin"
PermissionGroupRegular = "pg_regular_user"
PermissionGroupUserAdmin = "pg_user_admin"
PermissionGroupPermissionAdmin = "pg_permission_group_admin"
PermissionGroupDomainAdmin = "pg_domain_admin"
PermissionGroupDNSAdmin = "pg_dns_admin"
PermissionGroupMailboxAdmin = "pg_mailbox_admin"
PermissionGroupAliasAdmin = "pg_alias_admin"
PermissionGroupMessageAudit = "pg_message_audit"
PermissionGroupSystemAdmin = "pg_system_admin"
)
2026-06-22 14:34:57 +08:00
var legacyDefaultPermissionGroupIDs = [] string {
PermissionGroupUserAdmin ,
PermissionGroupPermissionAdmin ,
PermissionGroupDomainAdmin ,
PermissionGroupDNSAdmin ,
PermissionGroupMailboxAdmin ,
PermissionGroupAliasAdmin ,
PermissionGroupMessageAudit ,
PermissionGroupSystemAdmin ,
}
2026-06-22 14:31:10 +08:00
type PermissionInfo struct {
Key string `json:"key"`
Label string `json:"label"`
Description string `json:"description"`
Category string `json:"category"`
}
type PermissionGroupSummary struct {
ID string `json:"id"`
Name string `json:"name"`
}
type PermissionGroup struct {
ID string `json:"id"`
Name string `json:"name"`
Description string `json:"description"`
Permissions [] string `json:"permissions"`
System bool `json:"system"`
UserCount int `json:"userCount"`
CreatedAt time . Time `json:"createdAt"`
UpdatedAt time . Time `json:"updatedAt"`
}
var legacyPermissionExpansions = map [ string ][] string {
"admin.overview" : { PermissionAdminOverview },
"admin.users" : { PermissionUsersView , PermissionUsersCreate , PermissionUsersUpdate , PermissionUsersDelete , PermissionUsersResetPassword , PermissionGroupsView },
"admin.permission_groups" : { PermissionGroupsView , PermissionGroupsCreate , PermissionGroupsUpdate , PermissionGroupsDelete },
"admin.domains" : { PermissionDomainsView , PermissionDomainsCreate , PermissionDomainsUpdate , PermissionDomainsDelete },
"admin.dns" : { PermissionDomainsView , PermissionDNSView , PermissionDNSCheck },
"admin.mailboxes" : { PermissionUsersView , PermissionDomainsView , PermissionMailboxesView , PermissionMailboxesCreate , PermissionMailboxesUpdate , PermissionMailboxesDelete },
"admin.aliases" : { PermissionDomainsView , PermissionAliasesView , PermissionAliasesCreate , PermissionAliasesUpdate , PermissionAliasesDelete },
"admin.messages" : { PermissionMailboxesView , PermissionMessagesView , PermissionMessagesRead , PermissionMessagesAttachment },
"admin.settings" : { PermissionDomainsView , PermissionSettingsView , PermissionSettingsUpdate , PermissionSettingsTestSMTP , PermissionTemplatesView , PermissionTemplatesUpdate , PermissionTemplatesReset },
}
var permissionCatalogItems = [] PermissionInfo {
{ Key : PermissionAdminOverview , Label : "查看概览" , Description : "查看后台统计和首次配置检查。" , Category : "概览" },
{ Key : PermissionUsersView , Label : "查看用户" , Description : "查看用户列表、状态和绑定邮箱。" , Category : "用户" },
{ Key : PermissionUsersCreate , Label : "创建用户" , Description : "创建普通用户并分配权限组。" , Category : "用户" },
{ Key : PermissionUsersUpdate , Label : "编辑用户" , Description : "修改用户显示名称、状态和权限组。" , Category : "用户" },
{ Key : PermissionUsersDelete , Label : "删除用户" , Description : "删除非受保护用户。" , Category : "用户" },
{ Key : PermissionUsersResetPassword , Label : "重置用户密码" , Description : "为用户重置登录密码。" , Category : "用户" },
{ Key : PermissionGroupsView , Label : "查看权限组" , Description : "查看权限组、权限目录和使用人数。" , Category : "权限组" },
{ Key : PermissionGroupsCreate , Label : "创建权限组" , Description : "创建自定义权限组。" , Category : "权限组" },
{ Key : PermissionGroupsUpdate , Label : "编辑权限组" , Description : "修改自定义权限组名称、说明和权限。" , Category : "权限组" },
{ Key : PermissionGroupsDelete , Label : "删除权限组" , Description : "删除未被用户使用的自定义权限组。" , Category : "权限组" },
{ Key : PermissionDomainsView , Label : "查看域名" , Description : "查看邮件域名和 DKIM 配置。" , Category : "域名" },
{ Key : PermissionDomainsCreate , Label : "添加域名" , Description : "添加新的邮件域名。" , Category : "域名" },
{ Key : PermissionDomainsUpdate , Label : "启停域名" , Description : "启用或停用邮件域名。" , Category : "域名" },
{ Key : PermissionDomainsDelete , Label : "删除域名" , Description : "删除未被邮箱使用的域名。" , Category : "域名" },
{ Key : PermissionDNSView , Label : "查看 DNS" , Description : "查看域名需要配置的 DNS 记录。" , Category : "DNS" },
{ Key : PermissionDNSCheck , Label : "执行 DNS 检测" , Description : "触发 MX、SPF、DKIM、DMARC 检测。" , Category : "DNS" },
{ Key : PermissionMailboxesView , Label : "查看邮箱账号" , Description : "查看邮箱账号列表和归属用户。" , Category : "邮箱账号" },
{ Key : PermissionMailboxesCreate , Label : "创建邮箱账号" , Description : "创建邮箱账号并准备归属用户。" , Category : "邮箱账号" },
{ Key : PermissionMailboxesUpdate , Label : "编辑邮箱账号" , Description : "修改邮箱归属、显示名、配额和状态。" , Category : "邮箱账号" },
{ Key : PermissionMailboxesDelete , Label : "删除邮箱账号" , Description : "删除邮箱账号及关联邮件文件。" , Category : "邮箱账号" },
{ Key : PermissionAliasesView , Label : "查看别名转发" , Description : "查看别名转发规则。" , Category : "别名转发" },
{ Key : PermissionAliasesCreate , Label : "创建别名转发" , Description : "创建新的别名转发。" , Category : "别名转发" },
{ Key : PermissionAliasesUpdate , Label : "编辑别名转发" , Description : "修改别名转发来源、目标和启用状态。" , Category : "别名转发" },
{ Key : PermissionAliasesDelete , Label : "删除别名转发" , Description : "删除别名转发规则。" , Category : "别名转发" },
{ Key : PermissionMessagesView , Label : "查看邮件列表" , Description : "查看全局邮件列表和搜索结果。" , Category : "邮件审计" },
{ Key : PermissionMessagesRead , Label : "查看邮件正文" , Description : "查看任意邮箱及未注册收件人的邮件正文。" , Category : "邮件审计" },
{ Key : PermissionMessagesAttachment , Label : "下载邮件附件" , Description : "下载全局邮件中的附件。" , Category : "邮件审计" },
{ Key : PermissionSettingsView , Label : "查看系统设置" , Description : "查看系统、SMTP、安全和邮件设置。" , Category : "系统设置" },
{ Key : PermissionSettingsUpdate , Label : "修改系统设置" , Description : "保存系统、SMTP、安全和邮件设置。" , Category : "系统设置" },
{ Key : PermissionSettingsTestSMTP , Label : "测试 SMTP" , Description : "发送 SMTP 测试邮件。" , Category : "系统设置" },
{ Key : PermissionTemplatesView , Label : "查看邮件模板" , Description : "查看系统邮件模板。" , Category : "邮件模板" },
{ Key : PermissionTemplatesUpdate , Label : "编辑邮件模板" , Description : "修改系统邮件模板内容。" , Category : "邮件模板" },
{ Key : PermissionTemplatesReset , Label : "恢复邮件模板" , Description : "将系统邮件模板恢复默认。" , Category : "邮件模板" },
}
func permissionCatalog () [] PermissionInfo {
out := make ([] PermissionInfo , len ( permissionCatalogItems ))
copy ( out , permissionCatalogItems )
return out
}
func allPermissionKeys () [] string {
out := make ([] string , 0 , len ( permissionCatalogItems ))
for _ , item := range permissionCatalogItems {
out = append ( out , item . Key )
}
return out
}
func permissionSet () map [ string ] bool {
out := map [ string ] bool {}
for _ , item := range permissionCatalogItems {
out [ item . Key ] = true
}
return out
}
func addPermissionToSet ( item string , seen map [ string ] bool ) bool {
item = strings . TrimSpace ( item )
if item == "" {
return true
}
if permissionSet ()[ item ] {
seen [ item ] = true
return true
}
if expanded , ok := legacyPermissionExpansions [ item ]; ok {
for _ , permission := range expanded {
seen [ permission ] = true
}
return true
}
return false
}
func normalizePermissionList ( items [] string ) ([] string , error ) {
seen := map [ string ] bool {}
for _ , item := range items {
item = strings . TrimSpace ( item )
if ! addPermissionToSet ( item , seen ) {
return nil , fmt . Errorf ( "invalid permission: %s" , item )
}
}
out := make ([] string , 0 , len ( seen ))
for _ , item := range allPermissionKeys () {
if seen [ item ] {
out = append ( out , item )
}
}
return out , nil
}
func decodeStoredPermissions ( value string ) [] string {
var raw [] string
if err := json . Unmarshal ([] byte ( value ), & raw ); err != nil {
return nil
}
seen := map [ string ] bool {}
for _ , item := range raw {
addPermissionToSet ( item , seen )
}
out := make ([] string , 0 , len ( seen ))
for _ , item := range allPermissionKeys () {
if seen [ item ] {
out = append ( out , item )
}
}
return out
}
func encodePermissions ( items [] string ) string {
normalized , err := normalizePermissionList ( items )
if err != nil {
normalized = nil
}
data , _ := json . Marshal ( normalized )
return string ( data )
}
func defaultPermissionGroups () [] PermissionGroup {
return [] PermissionGroup {
{
ID : PermissionGroupSuperAdmin ,
Name : "超级管理员" ,
Description : "拥有全部后台权限,由用户身份决定,不通过权限组分配。" ,
Permissions : allPermissionKeys (),
System : true ,
},
{
ID : PermissionGroupRegular ,
Name : "普通用户" ,
Description : "仅可使用自己的邮箱功能,不包含后台权限。" ,
Permissions : [] string {},
System : true ,
},
}
}
func fixedPermissionGroupIDs () map [ string ] bool {
out := map [ string ] bool {}
for _ , group := range defaultPermissionGroups () {
out [ group . ID ] = true
}
return out
}
func assignablePermissionGroupIDs () map [ string ] bool {
out := fixedPermissionGroupIDs ()
delete ( out , PermissionGroupSuperAdmin )
delete ( out , PermissionGroupRegular )
return out
}
func isAssignablePermissionGroupID ( groupID string ) bool {
return groupID != "" && groupID != PermissionGroupSuperAdmin && groupID != PermissionGroupRegular
}
func permissionGroupOrder () map [ string ] int {
out := map [ string ] int {}
for index , group := range defaultPermissionGroups () {
out [ group . ID ] = index
}
return out
}
func permissionGroupNames () map [ string ] string {
out := map [ string ] string {}
for _ , group := range defaultPermissionGroups () {
out [ group . ID ] = group . Name
}
return out
}
func ( a * App ) ensureDefaultPermissionGroups ( ctx context . Context ) error {
now := a . now (). UTC (). Format ( time . RFC3339Nano )
for _ , item := range defaultPermissionGroups () {
if _ , err := a . db . ExecContext ( ctx , `UPDATE permission_groups SET name=name || ' (' || id || ')' WHERE name=? AND id<>?` , item . Name , item . ID ); err != nil {
return err
}
2026-06-22 14:42:37 +08:00
query := `INSERT INTO permission_groups(id,name,description,permissions_json,system,created_at,updated_at)
2026-06-22 14:31:10 +08:00
VALUES(?,?,?,?,?,?,?)
2026-06-22 14:42:37 +08:00
ON CONFLICT(id) DO UPDATE SET name=excluded.name, description=excluded.description, permissions_json=excluded.permissions_json, system=excluded.system, updated_at=excluded.updated_at`
if item . ID == PermissionGroupRegular {
query = `INSERT INTO permission_groups(id,name,description,permissions_json,system,created_at,updated_at)
VALUES(?,?,?,?,?,?,?)
ON CONFLICT(id) DO UPDATE SET name=excluded.name, description=excluded.description, system=excluded.system, updated_at=excluded.updated_at`
}
if _ , err := a . db . ExecContext ( ctx , query , item . ID , item . Name , item . Description , encodePermissions ( item . Permissions ), boolInt ( item . System ), now , now ); err != nil {
2026-06-22 14:31:10 +08:00
return err
}
}
2026-06-22 14:34:57 +08:00
if err := a . cleanupLegacyDefaultPermissionGroups ( ctx ); err != nil {
return err
}
return nil
}
func ( a * App ) cleanupLegacyDefaultPermissionGroups ( ctx context . Context ) error {
for _ , groupID := range legacyDefaultPermissionGroupIDs {
var userCount int
if err := a . db . QueryRowContext ( ctx , `SELECT COUNT(*) FROM user_permission_groups WHERE group_id=?` , groupID ). Scan ( & userCount ); err != nil {
return err
}
if userCount == 0 {
if _ , err := a . db . ExecContext ( ctx , `DELETE FROM permission_groups WHERE id=? AND system=1` , groupID ); err != nil {
return err
}
continue
}
if _ , err := a . db . ExecContext ( ctx , `UPDATE permission_groups SET system=0, updated_at=? WHERE id=? AND system=1` , a . now (). UTC (). Format ( time . RFC3339Nano ), groupID ); err != nil {
return err
}
}
2026-06-22 14:31:10 +08:00
return nil
}
func ( a * App ) attachUserAuthorization ( ctx context . Context , u * User ) error {
if u == nil {
return nil
}
permissions , err := a . permissionsForUser ( ctx , u . ID , u . Role )
if err != nil {
return err
}
groupIDs , groups , err := a . permissionGroupsForUser ( ctx , u . ID , u . Role )
if err != nil {
return err
}
u . Permissions = permissions
u . PermissionGroupIDs = groupIDs
u . PermissionGroups = groups
u . Protected = a . isDefaultAdminUser ( u )
return nil
}
func ( a * App ) permissionsForUser ( ctx context . Context , userID , role string ) ([] string , error ) {
if role == "admin" {
return allPermissionKeys (), nil
}
2026-06-22 14:42:37 +08:00
seen := map [ string ] bool {}
if err := a . addRegularGroupPermissions ( ctx , nil , seen ); err != nil {
return nil , err
}
2026-06-22 14:31:10 +08:00
rows , err := a . db . QueryContext ( ctx , `SELECT pg.id,pg.permissions_json
FROM permission_groups pg
JOIN user_permission_groups upg ON upg.group_id=pg.id
WHERE upg.user_id=?` , userID )
if err != nil {
return nil , err
}
defer rows . Close ()
for rows . Next () {
var groupID , raw string
if err := rows . Scan ( & groupID , & raw ); err != nil {
return nil , err
}
if ! isAssignablePermissionGroupID ( groupID ) {
continue
}
for _ , permission := range decodeStoredPermissions ( raw ) {
seen [ permission ] = true
}
}
if err := rows . Err (); err != nil {
return nil , err
}
out := make ([] string , 0 , len ( seen ))
for _ , item := range allPermissionKeys () {
if seen [ item ] {
out = append ( out , item )
}
}
return out , nil
}
2026-06-22 14:42:37 +08:00
func ( a * App ) addRegularGroupPermissions ( ctx context . Context , tx * sql . Tx , seen map [ string ] bool ) error {
var raw string
query := `SELECT permissions_json FROM permission_groups WHERE id=?`
var err error
if tx != nil {
err = tx . QueryRowContext ( ctx , query , PermissionGroupRegular ). Scan ( & raw )
} else {
err = a . db . QueryRowContext ( ctx , query , PermissionGroupRegular ). Scan ( & raw )
}
if errors . Is ( err , sql . ErrNoRows ) {
return nil
}
if err != nil {
return err
}
for _ , permission := range decodeStoredPermissions ( raw ) {
seen [ permission ] = true
}
return nil
}
func ( a * App ) effectivePermissionsForUserGroups ( ctx context . Context , tx * sql . Tx , groupIDs [] string ) ([] string , error ) {
seen := map [ string ] bool {}
if err := a . addRegularGroupPermissions ( ctx , tx , seen ); err != nil {
return nil , err
}
groupPermissions , err := a . permissionsForGroupIDs ( ctx , tx , groupIDs )
if err != nil {
return nil , err
}
for _ , permission := range groupPermissions {
seen [ permission ] = true
}
out := make ([] string , 0 , len ( seen ))
for _ , permission := range allPermissionKeys () {
if seen [ permission ] {
out = append ( out , permission )
}
}
return out , nil
}
2026-06-22 14:31:10 +08:00
func ( a * App ) permissionGroupsForUser ( ctx context . Context , userID , role string ) ([] string , [] PermissionGroupSummary , error ) {
if role == "admin" {
group := PermissionGroupSummary { ID : PermissionGroupSuperAdmin , Name : "超级管理员" }
return [] string { group . ID }, [] PermissionGroupSummary { group }, nil
}
2026-06-22 14:42:37 +08:00
ids := [] string { PermissionGroupRegular }
groups := [] PermissionGroupSummary {{ ID : PermissionGroupRegular , Name : "普通用户" }}
if err := a . db . QueryRowContext ( ctx , `SELECT name FROM permission_groups WHERE id=?` , PermissionGroupRegular ). Scan ( & groups [ 0 ]. Name ); err != nil && ! errors . Is ( err , sql . ErrNoRows ) {
return nil , nil , err
}
2026-06-22 14:31:10 +08:00
rows , err := a . db . QueryContext ( ctx , `SELECT pg.id,pg.name
FROM permission_groups pg
JOIN user_permission_groups upg ON upg.group_id=pg.id
WHERE upg.user_id=?` , userID )
if err != nil {
return nil , nil , err
}
defer rows . Close ()
order := permissionGroupOrder ()
for rows . Next () {
var group PermissionGroupSummary
if err := rows . Scan ( & group . ID , & group . Name ); err != nil {
return nil , nil , err
}
if ! isAssignablePermissionGroupID ( group . ID ) {
continue
}
ids = append ( ids , group . ID )
groups = append ( groups , group )
}
if err := rows . Err (); err != nil {
return nil , nil , err
}
sort . SliceStable ( groups , func ( i , j int ) bool {
left , leftOK := order [ groups [ i ]. ID ]
right , rightOK := order [ groups [ j ]. ID ]
if leftOK && rightOK {
return left < right
}
if leftOK != rightOK {
return leftOK
}
return strings . ToLower ( groups [ i ]. Name ) < strings . ToLower ( groups [ j ]. Name )
})
sort . SliceStable ( ids , func ( i , j int ) bool {
left , leftOK := order [ ids [ i ]]
right , rightOK := order [ ids [ j ]]
if leftOK && rightOK {
return left < right
}
if leftOK != rightOK {
return leftOK
}
return ids [ i ] < ids [ j ]
})
return ids , groups , nil
}
func userHasPermission ( user * User , permission string ) bool {
if user == nil {
return false
}
if user . Role == "admin" {
return true
}
for _ , item := range user . Permissions {
if item == permission {
return true
}
}
return false
}
func userHasAnyPermission ( user * User , permissions ... string ) bool {
if user == nil {
return false
}
if user . Role == "admin" {
return true
}
for _ , permission := range permissions {
if userHasPermission ( user , permission ) {
return true
}
}
return false
}
func userHasAdminAccess ( user * User ) bool {
return userHasAnyPermission ( user , allPermissionKeys () ... )
}
func ( a * App ) requireAdminAccess ( next http . Handler ) http . Handler {
return http . HandlerFunc ( func ( w http . ResponseWriter , r * http . Request ) {
if ! userHasAdminAccess ( currentUser ( r )) {
respondError ( w , http . StatusForbidden , "admin permission required" )
return
}
next . ServeHTTP ( w , r )
})
}
func ( a * App ) requirePermission ( permission string ) func ( http . Handler ) http . Handler {
return func ( next http . Handler ) http . Handler {
return http . HandlerFunc ( func ( w http . ResponseWriter , r * http . Request ) {
if ! userHasPermission ( currentUser ( r ), permission ) {
respondError ( w , http . StatusForbidden , "permission required" )
return
}
next . ServeHTTP ( w , r )
})
}
}
func ( a * App ) requireAnyPermission ( permissions ... string ) func ( http . Handler ) http . Handler {
return func ( next http . Handler ) http . Handler {
return http . HandlerFunc ( func ( w http . ResponseWriter , r * http . Request ) {
if ! userHasAnyPermission ( currentUser ( r ), permissions ... ) {
respondError ( w , http . StatusForbidden , "permission required" )
return
}
next . ServeHTTP ( w , r )
})
}
}
func actorCanGrantPermissions ( actor * User , permissions [] string ) bool {
if actor == nil {
return false
}
if actor . Role == "admin" {
return true
}
for _ , permission := range permissions {
if ! userHasPermission ( actor , permission ) {
return false
}
}
return true
}
func ( a * App ) permissionsForGroupIDs ( ctx context . Context , tx * sql . Tx , groupIDs [] string ) ([] string , error ) {
seen := map [ string ] bool {}
for _ , groupID := range cleanIDList ( groupIDs ) {
if ! isAssignablePermissionGroupID ( groupID ) {
return nil , fmt . Errorf ( "permission group not assignable: %s" , groupID )
}
var raw string
query := `SELECT permissions_json FROM permission_groups WHERE id=?`
var err error
if tx != nil {
err = tx . QueryRowContext ( ctx , query , groupID ). Scan ( & raw )
} else {
err = a . db . QueryRowContext ( ctx , query , groupID ). Scan ( & raw )
}
if err != nil {
if errors . Is ( err , sql . ErrNoRows ) {
return nil , fmt . Errorf ( "permission group not found: %s" , groupID )
}
return nil , err
}
for _ , permission := range decodeStoredPermissions ( raw ) {
seen [ permission ] = true
}
}
out := make ([] string , 0 , len ( seen ))
for _ , permission := range allPermissionKeys () {
if seen [ permission ] {
out = append ( out , permission )
}
}
return out , nil
}
func ( a * App ) setUserPermissionGroups ( ctx context . Context , tx * sql . Tx , userID string , groupIDs [] string , actor * User ) error {
groupIDs = cleanIDList ( groupIDs )
for _ , groupID := range groupIDs {
if ! isAssignablePermissionGroupID ( groupID ) {
return fmt . Errorf ( "permission group not assignable: %s" , groupID )
}
}
2026-06-22 14:42:37 +08:00
groupPermissions , err := a . effectivePermissionsForUserGroups ( ctx , tx , groupIDs )
2026-06-22 14:31:10 +08:00
if err != nil {
return err
}
if ! actorCanGrantPermissions ( actor , groupPermissions ) {
return errors . New ( "cannot assign permissions you do not hold" )
}
exec := func ( query string , args ... any ) error {
var err error
if tx != nil {
_ , err = tx . ExecContext ( ctx , query , args ... )
} else {
_ , err = a . db . ExecContext ( ctx , query , args ... )
}
return err
}
if err := exec ( `DELETE FROM user_permission_groups WHERE user_id=?` , userID ); err != nil {
return err
}
now := a . now (). UTC (). Format ( time . RFC3339Nano )
for _ , groupID := range groupIDs {
if err := exec ( `INSERT INTO user_permission_groups(user_id,group_id,created_at) VALUES(?,?,?)` , userID , groupID , now ); err != nil {
return err
}
}
return nil
}
func ( a * App ) permissionGroupByID ( ctx context . Context , id string ) ( * PermissionGroup , error ) {
row := a . db . QueryRowContext ( ctx , `SELECT pg.id,pg.name,pg.description,pg.permissions_json,pg.system,pg.created_at,pg.updated_at,COUNT(upg.user_id)
FROM permission_groups pg
LEFT JOIN user_permission_groups upg ON upg.group_id=pg.id
WHERE pg.id=?
GROUP BY pg.id,pg.name,pg.description,pg.permissions_json,pg.system,pg.created_at,pg.updated_at` , id )
var group PermissionGroup
var raw , created , updated string
var system int
if err := row . Scan ( & group . ID , & group . Name , & group . Description , & raw , & system , & created , & updated , & group . UserCount ); err != nil {
return nil , err
}
group . Permissions = decodeStoredPermissions ( raw )
group . System = intBool ( system )
group . CreatedAt = parseTime ( created )
group . UpdatedAt = parseTime ( updated )
return & group , nil
}
func ( a * App ) isDefaultAdminUser ( u * User ) bool {
if u == nil {
return false
}
adminEmail := normalizeEmail ( a . cfg . AdminEmail )
return adminEmail != "" && strings . EqualFold ( normalizeEmail ( u . Email ), adminEmail )
}
func sortPermissionGroups ( items [] PermissionGroup ) {
order := permissionGroupOrder ()
sort . SliceStable ( items , func ( i , j int ) bool {
left , leftOK := order [ items [ i ]. ID ]
right , rightOK := order [ items [ j ]. ID ]
if leftOK && rightOK {
return left < right
}
if leftOK != rightOK {
return leftOK
}
return strings . ToLower ( items [ i ]. Name ) < strings . ToLower ( items [ j ]. Name )
})
}