229 lines
7.1 KiB
Go
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, "接口不存在")
|
|
}
|