Scert/backend/internal/repository/project_members.go

109 lines
3.7 KiB
Go

package repository
import (
"context"
"database/sql"
"errors"
"fmt"
"strings"
"git.pcux.cn/xuans/Scert/backend/internal/domain"
)
var ErrProjectAccessDenied = errors.New("无权访问项目或执行此操作")
type ProjectAuthorizer interface {
AuthorizeProject(context.Context, string, string, domain.ProjectPermission) error
}
type SQLProjectMemberRepository struct {
db *sql.DB
driver string
}
var _ ProjectAuthorizer = (*SQLProjectMemberRepository)(nil)
func NewSQLProjectMemberRepository(db *sql.DB, driver ...string) *SQLProjectMemberRepository {
return &SQLProjectMemberRepository{db: db, driver: repositoryDriver(driver)}
}
func (r *SQLProjectMemberRepository) ensureDB() error {
if r == nil || r.db == nil {
return errors.New("数据库连接不能为空")
}
return nil
}
// AuthorizeProject 每次从数据库读取账户、项目和成员关系,不信任请求中的角色或用户分组。
// 这里只判定指定项目,业务入口仍须从真实资源取得项目,并在执行副作用前重新判权。
func (r *SQLProjectMemberRepository) AuthorizeProject(ctx context.Context, userID, projectID string, permission domain.ProjectPermission) error {
if err := r.ensureDB(); err != nil {
return err
}
if !permission.Valid() || strings.TrimSpace(userID) == "" || strings.TrimSpace(projectID) == "" {
return ErrProjectAccessDenied
}
rows, err := r.db.QueryContext(ctx, Rebind(r.driver, `SELECT u.role, u.status, COALESCE(m.role, '')
FROM users u JOIN projects p ON p.id = ?
LEFT JOIN project_members m ON m.project_id = p.id AND m.user_id = u.id
WHERE u.id = ?`), strings.TrimSpace(projectID), strings.TrimSpace(userID))
if err != nil {
return fmt.Errorf("读取项目权限失败:%w", err)
}
defer rows.Close()
allowed := false
for rows.Next() {
var userRole, status string
var role domain.ProjectRole
if err := rows.Scan(&userRole, &status, &role); err != nil {
return fmt.Errorf("读取项目成员失败:%w", err)
}
if status != UserStatusActive {
continue
}
switch userRole {
case UserRoleSuperAdmin:
allowed = true
case UserRoleAdmin, UserRoleUser:
// 普通账户即使具有项目管理角色,也不能直接读取完整证书私钥。
if permission == domain.PermissionPrivateKeyRead && userRole == UserRoleUser {
continue
}
allowed = allowed || role.Allows(permission)
}
}
if err := rows.Err(); err != nil {
return fmt.Errorf("读取项目权限结果失败:%w", err)
}
if !allowed {
return ErrProjectAccessDenied
}
return nil
}
// 授权与写入在同一条语句内判定,避免先检查再写入之间撤权导致越权。
// 系统管理员必须是当前有效超级管理员;普通管理员不会因用户组或全局角色自动获得项目权限。
const projectManagerPredicate = `EXISTS (SELECT 1 FROM users a WHERE a.id = ? AND a.status = 'active'
AND (a.role = 'super_admin' OR (a.role IN ('admin', 'user') AND EXISTS
(SELECT 1 FROM project_members manager WHERE manager.project_id = ?
AND manager.user_id = a.id AND manager.role = 'project_admin'))))`
func (r *SQLProjectMemberRepository) GrantProjectRole(ctx context.Context, actorID, projectID, userID string, role domain.ProjectRole) error {
return r.changeProjectRole(ctx, actorID, projectID, userID, role, true)
}
func (r *SQLProjectMemberRepository) RevokeProjectRole(ctx context.Context, actorID, projectID, userID string, role domain.ProjectRole) error {
return r.changeProjectRole(ctx, actorID, projectID, userID, role, false)
}
func projectMemberWriteResult(result sql.Result) error {
count, err := result.RowsAffected()
if err != nil {
return fmt.Errorf("读取项目成员变更结果失败:%w", err)
}
if count != 1 {
return ErrProjectAccessDenied
}
return nil
}