Scout/internal/httpapi/projects_access.go
2026-08-31 21:19:15 +08:00

229 lines
7.1 KiB
Go

package httpapi
import (
"net/http"
"strings"
"time"
"scout/internal/access"
"scout/internal/users"
)
func (s *Server) requireProjectAccess(writer http.ResponseWriter, request *http.Request, projectID, required string) bool {
if s.Access == nil || !s.Access.HasAccessControl(projectID) {
if required == access.RoleViewer {
return true
}
if _, ok := s.uploadUser(request); ok {
return true
}
if _, loggedIn := s.authenticatedUser(request); loggedIn {
writeError(writer, http.StatusForbidden, "需要项目开发者或管理员权限")
} else {
writeError(writer, http.StatusUnauthorized, "请先登录项目开发者账户")
}
return false
}
if user, ok := s.authenticatedUser(request); ok && user.Role == users.RoleAdmin {
return true
}
if tokenValue := projectTokenValue(request); tokenValue != "" {
if token, ok := s.Access.AuthenticateToken(projectID, tokenValue); ok {
if access.Allows(token.Role, required) {
return true
}
writeError(writer, http.StatusForbidden, "项目令牌权限不足")
return false
}
}
if user, ok := s.authenticatedUser(request); ok {
member, memberOK := s.Access.GetMember(projectID, user.ID)
if memberOK && access.Allows(member.Role, required) {
return true
}
writeError(writer, http.StatusForbidden, "没有执行此项目操作的权限")
return false
}
writeError(writer, http.StatusUnauthorized, "请先登录或提供项目令牌")
return false
}
func (s *Server) projectUploadUser(request *http.Request, projectID string) (users.User, bool) {
if s.Access != nil {
if tokenValue := projectTokenValue(request); tokenValue != "" {
if token, ok := s.Access.AuthenticateToken(projectID, tokenValue); ok && access.Allows(token.Role, access.RoleDeveloper) {
return users.User{
ID: "project-token:" + token.ID,
Name: "项目令牌",
Role: users.RoleDeveloper,
Active: true,
}, true
}
}
}
if user, ok := s.authenticatedUser(request); ok {
if user.Role == users.RoleAdmin {
return user, true
}
if s.Access != nil {
if member, memberOK := s.Access.GetMember(projectID, user.ID); memberOK && access.Allows(member.Role, access.RoleDeveloper) {
return user, true
}
}
}
return s.uploadUser(request)
}
func projectTokenValue(request *http.Request) string {
if value := strings.TrimSpace(request.Header.Get("X-Project-Token")); value != "" {
return value
}
value := strings.TrimSpace(request.Header.Get("Authorization"))
if strings.HasPrefix(value, "Bearer ") {
return strings.TrimSpace(strings.TrimPrefix(value, "Bearer "))
}
return ""
}
func (s *Server) requireProjectAdmin(writer http.ResponseWriter, request *http.Request, projectID string) bool {
if s.Access == nil || !s.Access.HasAccessControl(projectID) {
return s.requireRole(writer, request, users.RoleAdmin)
}
if user, ok := s.authenticatedUser(request); ok && user.Role == users.RoleAdmin {
return true
}
if user, ok := s.authenticatedUser(request); ok {
member, memberOK := s.Access.GetMember(projectID, user.ID)
if memberOK && member.Role == access.RoleAdmin {
return true
}
writeError(writer, http.StatusForbidden, "没有管理此项目的权限")
return false
}
writeError(writer, http.StatusUnauthorized, "请先登录项目管理员账户")
return false
}
func (s *Server) projectMembersRoute(writer http.ResponseWriter, request *http.Request, projectID string, rest []string) {
if s.Access == nil {
writeError(writer, http.StatusServiceUnavailable, "项目权限管理尚未启用")
return
}
if !s.requireProjectAdmin(writer, request, projectID) {
return
}
if len(rest) == 0 && request.Method == http.MethodGet {
writeJSON(writer, http.StatusOK, map[string]any{"members": s.Access.ListMembers(projectID)})
return
}
if len(rest) == 0 && request.Method == http.MethodPost {
var input struct {
UserID string `json:"user_id"`
Role string `json:"role"`
}
if err := decodeJSON(request, &input); err != nil {
writeError(writer, http.StatusBadRequest, err.Error())
return
}
if s.Users == nil {
writeError(writer, http.StatusServiceUnavailable, "用户管理尚未启用")
return
}
if _, ok := s.Users.Get(strings.TrimSpace(input.UserID)); !ok {
writeError(writer, http.StatusNotFound, "用户不存在")
return
}
member, err := s.Access.AddMember(projectID, input.UserID, input.Role)
if err != nil {
status := http.StatusBadRequest
if strings.Contains(err.Error(), "已经存在") {
status = http.StatusConflict
}
writeError(writer, status, err.Error())
return
}
writeJSON(writer, http.StatusCreated, member)
return
}
if len(rest) == 1 && request.Method == http.MethodPatch {
var input struct {
Role string `json:"role"`
}
if err := decodeJSON(request, &input); err != nil {
writeError(writer, http.StatusBadRequest, err.Error())
return
}
member, err := s.Access.UpdateMember(projectID, rest[0], input.Role)
if err != nil {
writeError(writer, http.StatusBadRequest, err.Error())
return
}
writeJSON(writer, http.StatusOK, member)
return
}
if len(rest) == 1 && request.Method == http.MethodDelete {
if err := s.Access.DeleteMember(projectID, rest[0]); err != nil {
writeError(writer, http.StatusNotFound, err.Error())
return
}
writer.WriteHeader(http.StatusNoContent)
return
}
writeError(writer, http.StatusNotFound, "接口不存在")
}
func (s *Server) projectTokensRoute(writer http.ResponseWriter, request *http.Request, projectID string, rest []string) {
if s.Access == nil {
writeError(writer, http.StatusServiceUnavailable, "项目权限管理尚未启用")
return
}
if !s.requireProjectAdmin(writer, request, projectID) {
return
}
if len(rest) == 0 && request.Method == http.MethodGet {
writeJSON(writer, http.StatusOK, map[string]any{"tokens": s.Access.ListTokens(projectID)})
return
}
if len(rest) == 0 && request.Method == http.MethodPost {
var input struct {
Name string `json:"name"`
Role string `json:"role"`
ExpiresAt string `json:"expires_at"`
}
if err := decodeJSON(request, &input); err != nil {
writeError(writer, http.StatusBadRequest, err.Error())
return
}
role := strings.TrimSpace(input.Role)
if role == "" {
role = access.RoleViewer
}
var expiresAt *time.Time
if strings.TrimSpace(input.ExpiresAt) != "" {
parsed, err := time.Parse(time.RFC3339, strings.TrimSpace(input.ExpiresAt))
if err != nil || !parsed.After(time.Now().UTC()) {
writeError(writer, http.StatusBadRequest, "令牌过期时间必须是未来的 RFC3339 时间")
return
}
expiresAt = &parsed
}
token, secret, err := s.Access.CreateToken(projectID, input.Name, role, expiresAt)
if err != nil {
writeError(writer, http.StatusBadRequest, err.Error())
return
}
writeJSON(writer, http.StatusCreated, map[string]any{"token": token, "secret": secret})
return
}
if len(rest) == 2 && rest[1] == "revoke" && request.Method == http.MethodPost {
token, err := s.Access.RevokeToken(projectID, rest[0])
if err != nil {
writeError(writer, http.StatusNotFound, err.Error())
return
}
writeJSON(writer, http.StatusOK, token)
return
}
writeError(writer, http.StatusNotFound, "接口不存在")
}