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, "接口不存在") }