Scert/backend/internal/repository/project_resources.go

197 lines
8.6 KiB
Go

package repository
import (
"context"
"database/sql"
"errors"
"fmt"
"strings"
"git.pcux.cn/xuans/Scert/backend/internal/deployment"
"git.pcux.cn/xuans/Scert/backend/internal/domain"
)
// ProjectResourceReader 将实时权限和资源读取合并,不向外部入口暴露全量基础仓储。
type ProjectResourceReader interface {
ListVisibleDomains(context.Context, string) ([]domain.Domain, error)
GetVisibleDomain(context.Context, string, string) (domain.Domain, error)
ListVisibleServers(context.Context, string) ([]deployment.Server, error)
GetVisibleServer(context.Context, string, string) (deployment.Server, error)
ListVisibleDomainServers(context.Context, string, string) ([]deployment.Server, error)
ListVisibleServerDomains(context.Context, string, string) ([]domain.Domain, error)
}
var _ ProjectResourceReader = (*SQLProjectMemberRepository)(nil)
const visibleDomainSelect = `SELECT domains.id, domains.name, domains.certificate_type,
domains.sans_json, domains.auto_renew, domains.acme_environment, domains.dns_provider,
domains.status, domains.expires_at, domains.created_at, domains.updated_at,
domains.project_id, domains.owner_id, domains.created_by, domains.updated_by, domains.challenge_type,
COALESCE((SELECT dns_account_id FROM domain_dns_accounts WHERE domain_id = domains.id AND project_id = domains.project_id), ''),
COALESCE((SELECT id FROM domain_dns_accounts WHERE domain_id = domains.id AND project_id = domains.project_id), '') FROM domains`
// 列名只接受内部固定值,角色能力复用领域模型;所有身份和角色值均使用绑定参数。
// EXISTS 避免多角色账户重复返回资源,不按用户分组或全局普通管理员身份放行。
func resourceReadPredicate(projectColumn, actorID string, permission domain.ProjectPermission) (string, []any) {
valid := projectColumn == "domains.project_id" && permission == domain.PermissionCertificateRead ||
projectColumn == "servers.project_id" && permission == domain.PermissionServerRead ||
projectColumn == "dns_accounts.project_id" && (permission == domain.PermissionDNSRead || permission == domain.PermissionDNSCredentialUse || permission == domain.PermissionDNSManage)
if !valid {
return "1 = 0", nil
}
roles := []domain.ProjectRole{
domain.ProjectRoleAdmin, domain.ProjectRoleCertificateAdmin, domain.ProjectRoleDeploymentAdmin,
domain.ProjectRoleDNSAdmin, domain.ProjectRoleAuditViewer, domain.ProjectRoleReadOnly,
}
args := []any{actorID}
placeholders := make([]string, 0, len(roles))
for _, role := range roles {
if role.Allows(permission) {
placeholders = append(placeholders, "?")
args = append(args, string(role))
}
}
return `EXISTS (SELECT 1 FROM projects resource_project
JOIN users resource_actor ON resource_actor.id = ?
WHERE resource_project.id = ` + projectColumn + ` AND resource_actor.status = 'active'
AND (resource_actor.role = 'super_admin' OR (resource_actor.role IN ('admin', 'user')
AND EXISTS (SELECT 1 FROM project_members resource_member
WHERE resource_member.project_id = resource_project.id AND resource_member.user_id = resource_actor.id
AND resource_member.role IN (` + strings.Join(placeholders, ", ") + `)))))`, args
}
func (r *SQLProjectMemberRepository) resourceReaderReady(actorID string) error {
if err := r.ensureDB(); err != nil {
return err
}
if strings.TrimSpace(actorID) == "" || len(actorID) > 128 {
return ErrProjectAccessDenied
}
return nil
}
func (r *SQLProjectMemberRepository) ListVisibleDomains(ctx context.Context, actorID string) ([]domain.Domain, error) {
if err := r.resourceReaderReady(actorID); err != nil {
return nil, err
}
predicate, args := resourceReadPredicate("domains.project_id", actorID, domain.PermissionCertificateRead)
rows, err := r.db.QueryContext(ctx, Rebind(r.driver, visibleDomainSelect+` WHERE `+predicate+` ORDER BY domains.name, domains.id`), args...)
if err != nil {
return nil, fmt.Errorf("查询可见域名失败:%w", err)
}
defer rows.Close()
items := make([]domain.Domain, 0)
for rows.Next() {
item, err := scanDomain(rows)
if err != nil {
return nil, err
}
items = append(items, item)
}
if err := rows.Err(); err != nil {
return nil, fmt.Errorf("读取可见域名失败:%w", err)
}
return items, nil
}
func (r *SQLProjectMemberRepository) GetVisibleDomain(ctx context.Context, actorID, id string) (domain.Domain, error) {
if err := r.resourceReaderReady(actorID); err != nil {
return domain.Domain{}, err
}
predicate, args := resourceReadPredicate("domains.project_id", actorID, domain.PermissionCertificateRead)
args = append(args, id)
item, err := scanDomain(r.db.QueryRowContext(ctx, Rebind(r.driver,
visibleDomainSelect+` WHERE `+predicate+` AND domains.id = ?`), args...))
if errors.Is(err, sql.ErrNoRows) {
return domain.Domain{}, ErrProjectAccessDenied
}
return item, err
}
func (r *SQLProjectMemberRepository) ListVisibleServers(ctx context.Context, actorID string) ([]deployment.Server, error) {
if err := r.resourceReaderReady(actorID); err != nil {
return nil, err
}
predicate, args := resourceReadPredicate("servers.project_id", actorID, domain.PermissionServerRead)
return r.readVisibleServers(ctx, serverSelectQualified+` WHERE `+predicate+` ORDER BY servers.name, servers.id`, args)
}
func (r *SQLProjectMemberRepository) GetVisibleServer(ctx context.Context, actorID, id string) (deployment.Server, error) {
if err := r.resourceReaderReady(actorID); err != nil {
return deployment.Server{}, err
}
predicate, args := resourceReadPredicate("servers.project_id", actorID, domain.PermissionServerRead)
args = append(args, id)
item, err := scanServer(r.db.QueryRowContext(ctx, Rebind(r.driver,
serverSelectQualified+` WHERE `+predicate+` AND servers.id = ?`), args...))
if errors.Is(err, sql.ErrNoRows) {
return deployment.Server{}, ErrProjectAccessDenied
}
return item, err
}
func (r *SQLProjectMemberRepository) ListVisibleDomainServers(ctx context.Context, actorID, domainID string) ([]deployment.Server, error) {
// 对未知和无权限域名统一拒绝;随后的查询仍重判双方权限,撤权后不会返回旧结果。
if _, err := r.GetVisibleDomain(ctx, actorID, domainID); err != nil {
return nil, err
}
domainPredicate, args := resourceReadPredicate("domains.project_id", actorID, domain.PermissionCertificateRead)
serverPredicate, serverArgs := resourceReadPredicate("servers.project_id", actorID, domain.PermissionServerRead)
args = append(args, serverArgs...)
args = append(args, domainID)
return r.readVisibleServers(ctx, serverSelectQualified+`
JOIN domain_servers visible_binding ON visible_binding.server_id = servers.id
JOIN domains ON domains.id = visible_binding.domain_id AND domains.project_id = servers.project_id
WHERE `+domainPredicate+` AND `+serverPredicate+` AND domains.id = ? ORDER BY servers.name, servers.id`, args)
}
func (r *SQLProjectMemberRepository) ListVisibleServerDomains(ctx context.Context, actorID, serverID string) ([]domain.Domain, error) {
if _, err := r.GetVisibleServer(ctx, actorID, serverID); err != nil {
return nil, err
}
domainPredicate, args := resourceReadPredicate("domains.project_id", actorID, domain.PermissionCertificateRead)
serverPredicate, serverArgs := resourceReadPredicate("servers.project_id", actorID, domain.PermissionServerRead)
args = append(args, serverArgs...)
args = append(args, serverID)
rows, err := r.db.QueryContext(ctx, Rebind(r.driver, visibleDomainSelect+`
JOIN domain_servers visible_binding ON visible_binding.domain_id = domains.id
JOIN servers ON servers.id = visible_binding.server_id AND servers.project_id = domains.project_id
WHERE `+domainPredicate+` AND `+serverPredicate+` AND servers.id = ? ORDER BY domains.name, domains.id`), args...)
if err != nil {
return nil, fmt.Errorf("failed to query visible server domains: %w", err)
}
defer rows.Close()
items := make([]domain.Domain, 0)
for rows.Next() {
item, err := scanDomain(rows)
if err != nil {
return nil, err
}
items = append(items, item)
}
if err := rows.Err(); err != nil {
return nil, fmt.Errorf("failed to read visible server domains: %w", err)
}
return items, nil
}
func (r *SQLProjectMemberRepository) readVisibleServers(ctx context.Context, query string, args []any) ([]deployment.Server, error) {
rows, err := r.db.QueryContext(ctx, Rebind(r.driver, query), args...)
if err != nil {
return nil, fmt.Errorf("查询可见服务器失败:%w", err)
}
defer rows.Close()
items := make([]deployment.Server, 0)
for rows.Next() {
item, err := scanServer(rows)
if err != nil {
return nil, err
}
items = append(items, item)
}
if err := rows.Err(); err != nil {
return nil, fmt.Errorf("读取可见服务器失败:%w", err)
}
return items, nil
}