console: add user token fn gateway

This commit is contained in:
Naeel
2026-04-27 11:24:36 +03:00
parent e5ce0794a4
commit feb4ae735d
5 changed files with 229 additions and 26 deletions
+18 -2
View File
@@ -15,6 +15,22 @@ import (
// Использует приватный тип чтобы избежать коллизий с ключами из других пакетов.
type ctxKeyNS struct{}
func authTokenFromRequest(r *http.Request) string {
token := strings.TrimSpace(r.Header.Get("X-Auth-Token"))
if token != "" {
return token
}
authz := strings.TrimSpace(r.Header.Get("Authorization"))
if authz == "" {
return ""
}
const bearerPrefix = "Bearer "
if len(authz) >= len(bearerPrefix) && strings.EqualFold(authz[:len(bearerPrefix)], bearerPrefix) {
return strings.TrimSpace(authz[len(bearerPrefix):])
}
return ""
}
// userNS возвращает namespace пользователя из контекста запроса.
// Устанавливается в authMiddleware после успешной аутентификации.
func (s *Server) userNS(r *http.Request) string {
@@ -47,7 +63,7 @@ func (s *Server) authMiddleware(h http.HandlerFunc) http.HandlerFunc {
if sub != "" {
ns = namespaceFromSub(sub)
} else {
token := strings.TrimSpace(r.Header.Get("X-Auth-Token"))
token := authTokenFromRequest(r)
resolvedNS, err := s.resolveNamespaceForToken(token, env, true)
if err != nil {
writeJSONError(w, http.StatusUnauthorized, "unauthorized")
@@ -56,7 +72,7 @@ func (s *Server) authMiddleware(h http.HandlerFunc) http.HandlerFunc {
ns = resolvedNS
}
} else {
token := strings.TrimSpace(r.Header.Get("X-Auth-Token"))
token := authTokenFromRequest(r)
resolvedNS, err := s.resolveNamespaceForToken(token, env, false)
if err != nil {
writeJSONError(w, http.StatusUnauthorized, "unauthorized")
+176
View File
@@ -70,6 +70,78 @@ func normalizeFunctionTimeout(seconds int64) int64 {
return seconds
}
func normalizeRoute(route string) string {
route = strings.TrimSpace(route)
if route == "" || route == "/" {
return "/"
}
if !strings.HasPrefix(route, "/") {
return "/" + route
}
return route
}
func routeAllowsMethod(methods []string, method string) bool {
if len(methods) == 0 {
return true
}
for _, candidate := range methods {
if strings.EqualFold(strings.TrimSpace(candidate), method) {
return true
}
}
return false
}
func appendUniqueMethods(dst []string, src []string) []string {
for _, method := range src {
method = strings.ToUpper(strings.TrimSpace(method))
if method == "" {
continue
}
seen := false
for _, existing := range dst {
if existing == method {
seen = true
break
}
}
if !seen {
dst = append(dst, method)
}
}
return dst
}
func shouldForwardRequestBody(method string) bool {
switch method {
case http.MethodGet, http.MethodHead:
return false
default:
return true
}
}
func copyProxyRequestHeaders(dst, src http.Header) {
for key, values := range src {
switch http.CanonicalHeaderKey(key) {
case "Authorization", "X-Auth-Token", "X-Auth-Env", "Host", "Content-Length":
continue
}
for _, value := range values {
dst.Add(key, value)
}
}
}
func copyProxyResponseHeaders(dst, src http.Header) {
for key, values := range src {
for _, value := range values {
dst.Add(key, value)
}
}
}
// handleFunctionsRoot обрабатывает запросы к /console/api/functions без имени функции.
// GET → список всех функций, POST → создать новую.
func (s *Server) handleFunctionsRoot(w http.ResponseWriter, r *http.Request) {
@@ -615,6 +687,110 @@ func (s *Server) handleInvokeFunction(w http.ResponseWriter, r *http.Request, na
})
}
// handleInvokeRoute даёт пользователю прямой HTTP gateway к своей функции по route.
// Внешний контракт: /fn/<route> + Authorization: Bearer <user-token>.
func (s *Server) handleInvokeRoute(w http.ResponseWriter, r *http.Request) {
route := normalizeRoute(strings.TrimPrefix(r.URL.Path, "/fn"))
if route == "/" {
writeJSONError(w, http.StatusBadRequest, "route is required")
return
}
ns := s.userNS(r)
lookupCtx, lookupCancel := context.WithTimeout(r.Context(), 10*time.Second)
defer lookupCancel()
triggers, err := s.dyn.Resource(fission.HTTPTrigGVR).Namespace(ns).List(lookupCtx, metav1.ListOptions{})
if err != nil {
writeJSONError(w, http.StatusBadGateway, fmt.Sprintf("list httptriggers: %v", err))
return
}
matchedFunction := ""
allowedMethods := make([]string, 0, 4)
for _, trig := range triggers.Items {
trigRoute, _, _ := unstructured.NestedString(trig.Object, "spec", "relativeurl")
if normalizeRoute(trigRoute) != route {
continue
}
methods, _, _ := unstructured.NestedStringSlice(trig.Object, "spec", "methods")
allowedMethods = appendUniqueMethods(allowedMethods, methods)
if !routeAllowsMethod(methods, r.Method) {
continue
}
matchedFunction, _, _ = unstructured.NestedString(trig.Object, "spec", "functionref", "name")
if matchedFunction != "" {
break
}
}
if matchedFunction == "" {
if len(allowedMethods) > 0 {
w.Header().Set("Allow", strings.Join(allowedMethods, ", "))
writeJSONError(w, http.StatusMethodNotAllowed, fmt.Sprintf("route %q does not allow method %s", route, r.Method))
return
}
writeJSONError(w, http.StatusNotFound, fmt.Sprintf("route %q not found", route))
return
}
fn, err := s.dyn.Resource(fission.FunctionGVR).Namespace(ns).Get(lookupCtx, matchedFunction, metav1.GetOptions{})
if err != nil {
writeJSONError(w, http.StatusBadGateway, fmt.Sprintf("get function %q: %v", matchedFunction, err))
return
}
bodyBytes, err := io.ReadAll(r.Body)
if err != nil {
writeJSONError(w, http.StatusBadRequest, fmt.Sprintf("read request body: %v", err))
return
}
invokeTimeout := s.resolveInvokeTimeout(fn)
ctx, cancel := context.WithTimeout(r.Context(), invokeTimeout)
defer cancel()
invokeURL := s.routerURL + route
if r.URL.RawQuery != "" {
invokeURL += "?" + r.URL.RawQuery
}
var invokeBody io.Reader
if shouldForwardRequestBody(r.Method) {
invokeBody = bytes.NewReader(bodyBytes)
}
req, err := http.NewRequestWithContext(ctx, r.Method, invokeURL, invokeBody)
if err != nil {
writeJSONError(w, http.StatusInternalServerError, fmt.Sprintf("build invoke request: %v", err))
return
}
copyProxyRequestHeaders(req.Header, r.Header)
if token := s.getRouterToken(); token != "" {
req.Header.Set("Authorization", "Bearer "+token)
}
resp, err := s.http.Do(req)
if err != nil {
if errors.Is(err, context.DeadlineExceeded) {
writeJSONError(w, http.StatusBadGateway, fmt.Sprintf("invoke route %q timeout after %s", route, invokeTimeout))
return
}
var netErr net.Error
if errors.As(err, &netErr) && netErr.Timeout() {
writeJSONError(w, http.StatusBadGateway, fmt.Sprintf("invoke route %q timeout after %s", route, invokeTimeout))
return
}
writeJSONError(w, http.StatusBadGateway, fmt.Sprintf("invoke route %q: %v", route, err))
return
}
defer resp.Body.Close()
copyProxyResponseHeaders(w.Header(), resp.Header)
w.WriteHeader(resp.StatusCode)
_, _ = io.Copy(w, resp.Body)
}
// handleDeleteFunction удаляет функцию и связанные объекты: HTTPTrigger, Package.
// После удаления вызывает CleanupEnvironmentIfUnused — убирает environment если язык больше не используется.
func (s *Server) handleDeleteFunction(w http.ResponseWriter, r *http.Request, name string) {
+1
View File
@@ -144,6 +144,7 @@ func (s *Server) RegisterRoutes(mux *http.ServeMux) {
mux.HandleFunc("/console/api/packages", auth(s.handleList(fission.PackageGVR)))
mux.HandleFunc("/console/api/functions", auth(s.handleFunctionsRoot))
mux.HandleFunc("/console/api/functions/", auth(s.handleFunctionsAction))
mux.HandleFunc("/fn/", auth(s.handleInvokeRoute))
mux.HandleFunc("/console/api/httptriggers", auth(s.handleList(fission.HTTPTrigGVR)))
mux.HandleFunc("/console/api/timetriggers", auth(s.handleList(fission.TimeTrigGVR)))
mux.HandleFunc("/console/api/ns/status", auth(s.handleNSStatus))