package app import ( "context" "database/sql" "encoding/json" "errors" "fmt" "net/http" "sort" "strings" "time" ) const ( PermissionMailAccess = "mail.access" PermissionMailRead = "mail.messages.read" PermissionMailSend = "mail.messages.send" PermissionMailDrafts = "mail.messages.drafts" PermissionMailSchedule = "mail.messages.schedule" PermissionMailOrganize = "mail.messages.organize" PermissionMailLabels = "mail.labels.manage" PermissionMailAttachments = "mail.attachments.download" PermissionMailContacts = "mail.contacts.manage" PermissionMailSignatures = "mail.signatures.manage" PermissionMailRules = "mail.rules.manage" PermissionMailBlocked = "mail.blocked_senders.manage" PermissionMailStats = "mail.stats.view" PermissionMailboxApply = "mail.mailboxes.apply" 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 regularUserPermissionMigrationKey = "permission_groups.regular_mail_permissions_v1" 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" ) var legacyDefaultPermissionGroupIDs = []string{ "pg_permission_manager", "pg_user_manager", "pg_system_operator", "pg_mail_operator", PermissionGroupUserAdmin, PermissionGroupPermissionAdmin, PermissionGroupDomainAdmin, PermissionGroupDNSAdmin, PermissionGroupMailboxAdmin, PermissionGroupAliasAdmin, PermissionGroupMessageAudit, PermissionGroupSystemAdmin, } 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"` Limits PermissionLimits `json:"limits"` System bool `json:"system"` UserCount int `json:"userCount"` CreatedAt time.Time `json:"createdAt"` UpdatedAt time.Time `json:"updatedAt"` } type PermissionLimits struct { MaxAttachmentMB int `json:"maxAttachmentMb"` MaxMailboxCount int `json:"maxMailboxCount"` SMTPDailyLimit int `json:"smtpDailyLimit"` SMTPMinuteLimit int `json:"smtpMinuteLimit"` IMAPMinuteLimit int `json:"imapMinuteLimit"` POP3MinuteLimit int `json:"pop3MinuteLimit"` } func defaultPermissionLimits() PermissionLimits { return PermissionLimits{ MaxAttachmentMB: 25, MaxMailboxCount: 9, SMTPDailyLimit: 200, SMTPMinuteLimit: 20, IMAPMinuteLimit: 200, POP3MinuteLimit: 150, } } func normalizePermissionLimits(limits PermissionLimits) (PermissionLimits, error) { if limits.MaxAttachmentMB < 0 { return PermissionLimits{}, errors.New("maxAttachmentMb cannot be negative") } if limits.MaxMailboxCount < 0 { return PermissionLimits{}, errors.New("maxMailboxCount cannot be negative") } if limits.SMTPDailyLimit < 0 { return PermissionLimits{}, errors.New("smtpDailyLimit cannot be negative") } if limits.SMTPMinuteLimit < 0 { return PermissionLimits{}, errors.New("smtpMinuteLimit cannot be negative") } if limits.IMAPMinuteLimit < 0 { return PermissionLimits{}, errors.New("imapMinuteLimit cannot be negative") } if limits.POP3MinuteLimit < 0 { return PermissionLimits{}, errors.New("pop3MinuteLimit cannot be negative") } return limits, nil } func normalizeMailboxLimitOverride(value *int) (*int, error) { if value == nil { return nil, nil } if *value < 0 { return nil, errors.New("mailboxLimitOverride cannot be negative") } normalized := *value return &normalized, nil } func decodeStoredLimits(value string) PermissionLimits { limits := defaultPermissionLimits() if strings.TrimSpace(value) == "" { return limits } _ = json.Unmarshal([]byte(value), &limits) normalized, err := normalizePermissionLimits(limits) if err != nil { return defaultPermissionLimits() } return normalized } func encodePermissionLimits(limits PermissionLimits) string { normalized, err := normalizePermissionLimits(limits) if err != nil { normalized = defaultPermissionLimits() } data, _ := json.Marshal(normalized) return string(data) } func mergePermissionLimits(left, right PermissionLimits) PermissionLimits { return PermissionLimits{ MaxAttachmentMB: mergeLimitValue(left.MaxAttachmentMB, right.MaxAttachmentMB), MaxMailboxCount: mergeLimitValue(left.MaxMailboxCount, right.MaxMailboxCount), SMTPDailyLimit: mergeLimitValue(left.SMTPDailyLimit, right.SMTPDailyLimit), SMTPMinuteLimit: mergeLimitValue(left.SMTPMinuteLimit, right.SMTPMinuteLimit), IMAPMinuteLimit: mergeLimitValue(left.IMAPMinuteLimit, right.IMAPMinuteLimit), POP3MinuteLimit: mergeLimitValue(left.POP3MinuteLimit, right.POP3MinuteLimit), } } func mergeLimitValue(left, right int) int { if left == 0 || right == 0 { return 0 } if right > left { return right } return left } func minimalLimits() PermissionLimits { // minimalLimits sets every field to 1 so that mergePermissionLimits // (which takes the max of each field) produces correct aggregation // when no group has a limit set for a given field. return PermissionLimits{ MaxAttachmentMB: 1, MaxMailboxCount: 1, SMTPDailyLimit: 1, SMTPMinuteLimit: 1, IMAPMinuteLimit: 1, POP3MinuteLimit: 1, } } func actorCanGrantLimits(actor *User, limits PermissionLimits) bool { if actor == nil { return false } if actor.Role == "admin" { return true } return canGrantLimitValue(actor.Limits.MaxAttachmentMB, limits.MaxAttachmentMB) && canGrantLimitValue(actor.Limits.MaxMailboxCount, limits.MaxMailboxCount) && canGrantLimitValue(actor.Limits.SMTPDailyLimit, limits.SMTPDailyLimit) && canGrantLimitValue(actor.Limits.SMTPMinuteLimit, limits.SMTPMinuteLimit) && canGrantLimitValue(actor.Limits.IMAPMinuteLimit, limits.IMAPMinuteLimit) && canGrantLimitValue(actor.Limits.POP3MinuteLimit, limits.POP3MinuteLimit) } func canGrantLimitValue(actorLimit, requestedLimit int) bool { if actorLimit == 0 { return true } if requestedLimit == 0 { return false } return requestedLimit <= actorLimit } func attachmentLimitBytes(limits PermissionLimits) int64 { if limits.MaxAttachmentMB <= 0 { return 0 } return int64(limits.MaxAttachmentMB) * 1024 * 1024 } var legacyPermissionExpansions = map[string][]string{ "mail": regularUserDefaultPermissions(), "mail.messages": {PermissionMailAccess, PermissionMailRead, PermissionMailSend, PermissionMailDrafts, PermissionMailSchedule, PermissionMailOrganize, PermissionMailAttachments}, "mail.labels": {PermissionMailAccess, PermissionMailRead, PermissionMailLabels}, "mail.contacts": {PermissionMailContacts}, "mail.signatures": {PermissionMailSignatures}, "mail.rules": {PermissionMailRules, PermissionMailBlocked}, "mail.mailboxes": {PermissionMailboxApply}, "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: PermissionMailAccess, Label: "访问邮箱前台", Description: "进入邮箱前台并查看本人邮箱列表。", Category: "邮箱前台"}, {Key: PermissionMailRead, Label: "查看本人邮件", Description: "查看本人文件夹、邮件列表、星标邮件和邮件正文。", Category: "邮箱前台"}, {Key: PermissionMailSend, Label: "发送邮件", Description: "使用本人邮箱发送、回复和转发邮件。", Category: "邮箱前台"}, {Key: PermissionMailDrafts, Label: "管理草稿", Description: "保存、编辑和删除本人草稿。", Category: "邮箱前台"}, {Key: PermissionMailSchedule, Label: "定时发送", Description: "创建、查看和取消本人定时发送任务。", Category: "邮箱前台"}, {Key: PermissionMailOrganize, Label: "整理邮件", Description: "标记已读、星标、移动、归档、删除和清理本人邮件。", Category: "邮箱前台"}, {Key: PermissionMailLabels, Label: "管理邮件标签", Description: "创建标签,并为本人邮件添加或移除标签。", Category: "邮箱前台"}, {Key: PermissionMailAttachments, Label: "下载附件", Description: "下载本人邮件中的附件。", Category: "邮箱前台"}, {Key: PermissionMailContacts, Label: "管理联系人", Description: "查看、新增和删除本人的联系人。", Category: "个人中心"}, {Key: PermissionMailSignatures, Label: "管理签名", Description: "查看、新增、修改和删除本人的邮件签名。", Category: "个人中心"}, {Key: PermissionMailRules, Label: "管理收件规则", Description: "查看、新增和删除本人的收件规则。", Category: "个人中心"}, {Key: PermissionMailBlocked, Label: "管理拦截名单", Description: "查看、新增和删除本人的发件人拦截规则。", Category: "个人中心"}, {Key: PermissionMailStats, Label: "查看邮箱统计", Description: "查看本人邮箱统计和清理概览。", Category: "个人中心"}, {Key: PermissionMailboxApply, Label: "自助申请邮箱", Description: "在开放申请时为本人申请邮箱。", Category: "个人中心"}, {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 adminPermissionKeys() []string { out := make([]string, 0, len(permissionCatalogItems)) for _, item := range permissionCatalogItems { if strings.HasPrefix(item.Key, "admin.") { 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(), Limits: PermissionLimits{}, System: true, }, { ID: PermissionGroupRegular, Name: "普通用户", Description: "仅可使用自己的邮箱功能,不包含后台权限。", Permissions: regularUserDefaultPermissions(), Limits: defaultPermissionLimits(), System: true, }, } } func regularUserDefaultPermissions() []string { return []string{ PermissionMailAccess, PermissionMailRead, PermissionMailSend, PermissionMailDrafts, PermissionMailSchedule, PermissionMailOrganize, PermissionMailLabels, PermissionMailAttachments, PermissionMailContacts, PermissionMailSignatures, PermissionMailRules, PermissionMailBlocked, PermissionMailStats, PermissionMailboxApply, } } 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 } query := `INSERT INTO permission_groups(id,name,description,permissions_json,limits_json,system,created_at,updated_at) VALUES(?,?,?,?,?,?,?,?) ON CONFLICT(id) DO UPDATE SET name=excluded.name, description=excluded.description, permissions_json=excluded.permissions_json, limits_json=excluded.limits_json, system=excluded.system, updated_at=excluded.updated_at` if item.ID == PermissionGroupRegular { query = `INSERT INTO permission_groups(id,name,description,permissions_json,limits_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), encodePermissionLimits(item.Limits), boolInt(item.System), now, now); err != nil { return err } } if err := a.cleanupLegacyDefaultPermissionGroups(ctx); err != nil { return err } if err := a.ensureRegularUserMailPermissions(ctx); err != nil { return err } return nil } func (a *App) ensureRegularUserMailPermissions(ctx context.Context) error { var migrated string err := a.db.QueryRowContext(ctx, `SELECT value FROM system_settings WHERE key=?`, regularUserPermissionMigrationKey).Scan(&migrated) if err == nil && migrated == "1" { return nil } if err != nil && !errors.Is(err, sql.ErrNoRows) { return err } var raw string if err := a.db.QueryRowContext(ctx, `SELECT permissions_json FROM permission_groups WHERE id=?`, PermissionGroupRegular).Scan(&raw); err != nil { return err } seen := map[string]bool{} for _, permission := range decodeStoredPermissions(raw) { seen[permission] = true } changed := false for _, permission := range regularUserDefaultPermissions() { if !seen[permission] { seen[permission] = true changed = true } } now := a.now().UTC().Format(time.RFC3339Nano) if changed { permissions := make([]string, 0, len(seen)) for _, permission := range allPermissionKeys() { if seen[permission] { permissions = append(permissions, permission) } } if _, err := a.db.ExecContext(ctx, `UPDATE permission_groups SET permissions_json=?, updated_at=? WHERE id=?`, encodePermissions(permissions), now, PermissionGroupRegular); err != nil { return err } } _, err = a.db.ExecContext(ctx, `INSERT INTO system_settings(key,value,updated_at) VALUES(?,?,?) ON CONFLICT(key) DO UPDATE SET value=excluded.value, updated_at=excluded.updated_at`, regularUserPermissionMigrationKey, "1", now) return err } 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 } } return nil } func (a *App) attachUserAuthorization(ctx context.Context, u *User) error { if u == nil { return nil } if u.LoginName == "" { u.LoginName = u.Email } permissions, err := a.permissionsForUser(ctx, u.ID, u.Role) if err != nil { return err } limits, err := a.limitsForUser(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.Limits = limits if u.Role != "admin" && u.MailboxLimitOverride != nil { u.Limits.MaxMailboxCount = *u.MailboxLimitOverride } 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 } seen := map[string]bool{} if err := a.addRegularGroupPermissions(ctx, nil, seen); err != nil { return nil, err } 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 } func (a *App) limitsForUser(ctx context.Context, userID, role string) (PermissionLimits, error) { if role == "admin" { return PermissionLimits{}, nil } limits, ok, err := a.regularGroupLimits(ctx, nil) if err != nil { return PermissionLimits{}, err } if !ok { limits = defaultPermissionLimits() } rows, err := a.db.QueryContext(ctx, `SELECT pg.id,pg.limits_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 PermissionLimits{}, err } defer rows.Close() for rows.Next() { var groupID, raw string if err := rows.Scan(&groupID, &raw); err != nil { return PermissionLimits{}, err } if !isAssignablePermissionGroupID(groupID) { continue } limits = mergePermissionLimits(limits, decodeStoredLimits(raw)) } if err := rows.Err(); err != nil { return PermissionLimits{}, err } return limits, nil } func (a *App) regularGroupLimits(ctx context.Context, tx *sql.Tx) (PermissionLimits, bool, error) { var raw string query := `SELECT limits_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 PermissionLimits{}, false, nil } if err != nil { return PermissionLimits{}, false, err } return decodeStoredLimits(raw), true, nil } 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 } func (a *App) effectiveLimitsForUserGroups(ctx context.Context, tx *sql.Tx, groupIDs []string) (PermissionLimits, error) { limits, ok, err := a.regularGroupLimits(ctx, tx) if err != nil { return PermissionLimits{}, err } if !ok { limits = defaultPermissionLimits() } groupLimits, err := a.limitsForGroupIDs(ctx, tx, groupIDs) if err != nil { return PermissionLimits{}, err } return mergePermissionLimits(limits, groupLimits), nil } 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 } 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 } 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, adminPermissionKeys()...) } 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) limitsForGroupIDs(ctx context.Context, tx *sql.Tx, groupIDs []string) (PermissionLimits, error) { limits := minimalLimits() for _, groupID := range cleanIDList(groupIDs) { if !isAssignablePermissionGroupID(groupID) { return PermissionLimits{}, fmt.Errorf("permission group not assignable: %s", groupID) } var raw string query := `SELECT limits_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 PermissionLimits{}, fmt.Errorf("permission group not found: %s", groupID) } return PermissionLimits{}, err } limits = mergePermissionLimits(limits, decodeStoredLimits(raw)) } return limits, 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) } } groupPermissions, err := a.effectivePermissionsForUserGroups(ctx, tx, groupIDs) if err != nil { return err } if !actorCanGrantPermissions(actor, groupPermissions) { return errors.New("cannot assign permissions you do not hold") } groupLimits, err := a.effectiveLimitsForUserGroups(ctx, tx, groupIDs) if err != nil { return err } if !actorCanGrantLimits(actor, groupLimits) { return errors.New("cannot assign limits above your own") } 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.limits_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.limits_json,pg.system,pg.created_at,pg.updated_at`, id) var group PermissionGroup var rawPermissions, rawLimits, created, updated string var system int if err := row.Scan(&group.ID, &group.Name, &group.Description, &rawPermissions, &rawLimits, &system, &created, &updated, &group.UserCount); err != nil { return nil, err } group.Permissions = decodeStoredPermissions(rawPermissions) group.Limits = decodeStoredLimits(rawLimits) 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) }) }