fix: invoke auth via Fission router JWT login

This commit is contained in:
Naeel
2026-04-15 07:48:56 +03:00
parent 6731e8998b
commit 8920ba10e6
3 changed files with 164 additions and 11 deletions
+11 -1
View File
@@ -46,7 +46,7 @@ spec:
serviceAccountName: fission-console
containers:
- name: console
image: naeel/fission-console:v0.1.5
image: naeel/fission-console:v0.2.2
ports:
- containerPort: 8090
env:
@@ -56,6 +56,16 @@ spec:
value: "http://router.fission.svc.cluster.local"
- name: PORT
value: "8090"
- name: FISSION_AUTH_USERNAME
valueFrom:
secretKeyRef:
name: router
key: username
- name: FISSION_AUTH_PASSWORD
valueFrom:
secretKeyRef:
name: router
key: password
livenessProbe:
httpGet:
path: /health
+81 -8
View File
@@ -11,6 +11,7 @@ import (
"net/http"
"os"
"strings"
"sync"
"time"
"fission-console/ui"
@@ -32,11 +33,21 @@ var (
timeTrigGVR = schema.GroupVersionResource{Group: "fission.io", Version: "v1", Resource: "timetriggers"}
)
const defaultSATokenPath = "/var/run/secrets/kubernetes.io/serviceaccount/token"
type server struct {
dyn dynamic.Interface
ns string
routerURL string
http *http.Client
dyn dynamic.Interface
ns string
routerURL string
http *http.Client
saTokenPath string
authUser string
authPass string
tokenMu sync.Mutex
cachedJWT string
tokenExpAt time.Time
}
type createFunctionRequest struct {
@@ -68,11 +79,18 @@ func main() {
log.Fatalf("create dynamic client: %v", err)
}
authUser := envDefault("FISSION_AUTH_USERNAME", "")
authPass := envDefault("FISSION_AUTH_PASSWORD", "")
saTokenPath := envDefault("SA_TOKEN_PATH", defaultSATokenPath)
s := &server{
dyn: dyn,
ns: namespace,
routerURL: routerURL,
http: &http.Client{Timeout: 30 * time.Second},
dyn: dyn,
ns: namespace,
routerURL: routerURL,
http: &http.Client{Timeout: 30 * time.Second},
saTokenPath: saTokenPath,
authUser: authUser,
authPass: authPass,
}
mux := http.NewServeMux()
@@ -474,6 +492,9 @@ func (s *server) handleInvokeFunction(w http.ResponseWriter, r *http.Request, na
if invokeMethod == http.MethodPost {
req.Header.Set("Content-Type", "application/json")
}
if token := s.getRouterToken(); token != "" {
req.Header.Set("Authorization", "Bearer "+token)
}
resp, err := s.http.Do(req)
if err != nil {
@@ -491,6 +512,58 @@ func (s *server) handleInvokeFunction(w http.ResponseWriter, r *http.Request, na
})
}
func (s *server) readSAToken() string {
if s.saTokenPath == "" {
return ""
}
data, err := os.ReadFile(s.saTokenPath)
if err != nil {
return ""
}
return strings.TrimSpace(string(data))
}
func (s *server) getRouterToken() string {
if s.authUser == "" || s.authPass == "" {
return s.readSAToken()
}
s.tokenMu.Lock()
defer s.tokenMu.Unlock()
if s.cachedJWT != "" && time.Now().Before(s.tokenExpAt) {
return s.cachedJWT
}
loginURL := s.routerURL + "/auth/login"
body, _ := json.Marshal(map[string]string{"username": s.authUser, "password": s.authPass})
resp, err := s.http.Post(loginURL, "application/json", bytes.NewReader(body))
if err != nil {
log.Printf("router login failed: %v", err)
return s.readSAToken()
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusCreated {
respBody, _ := io.ReadAll(resp.Body)
log.Printf("router login %d: %s", resp.StatusCode, string(respBody))
return s.readSAToken()
}
var result struct {
AccessToken string `json:"accesstoken"`
}
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil || result.AccessToken == "" {
log.Printf("router login decode error: %v", err)
return s.readSAToken()
}
s.cachedJWT = result.AccessToken
s.tokenExpAt = time.Now().Add(100 * time.Second)
log.Printf("router JWT obtained, expires in 100s")
return s.cachedJWT
}
func (s *server) handleDeleteFunction(w http.ResponseWriter, r *http.Request, name string) {
ctx, cancel := context.WithTimeout(r.Context(), 20*time.Second)
defer cancel()
+72 -2
View File
@@ -26,7 +26,7 @@ func newTestServer(objs ...runtime.Object) *server {
timeTrigGVR: "TimeTriggerList",
}
dyn := dynamicfake.NewSimpleDynamicClientWithCustomListKinds(runtime.NewScheme(), listKinds, objs...)
return &server{dyn: dyn, ns: "default", routerURL: "http://example.invalid", http: &http.Client{}}
return &server{dyn: dyn, ns: "default", routerURL: "http://example.invalid", http: &http.Client{}, saTokenPath: ""}
}
func TestNormalizeMethods(t *testing.T) {
@@ -109,7 +109,7 @@ func TestUpdateFunctionCode(t *testing.T) {
env := &unstructured.Unstructured{Object: map[string]any{
"apiVersion": "fission.io/v1",
"kind": "Environment",
"metadata": map[string]any{"name": "python", "namespace": "default"},
"metadata": map[string]any{"name": "python", "namespace": "default"},
}}
s := newTestServer(env)
@@ -141,3 +141,73 @@ func TestUpdateFunctionCode(t *testing.T) {
t.Fatalf("expected new-code, got %q", string(decoded))
}
}
func TestInvokeFunctionWithJWTAuth(t *testing.T) {
// Mock router: /auth/login returns JWT, /inv-fn returns hello
var gotAuth string
mockRouter := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path == "/auth/login" && r.Method == http.MethodPost {
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte(`{"accesstoken":"fake-jwt-token-xyz","tokentype":"Bearer"}`))
return
}
gotAuth = r.Header.Get("Authorization")
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte(`{"hello":"world"}`))
}))
defer mockRouter.Close()
env := &unstructured.Unstructured{Object: map[string]any{
"apiVersion": "fission.io/v1",
"kind": "Environment",
"metadata": map[string]any{"name": "python", "namespace": "default"},
}}
listKinds := map[schema.GroupVersionResource]string{
environmentGVR: "EnvironmentList",
packageGVR: "PackageList",
functionGVR: "FunctionList",
httpTrigGVR: "HTTPTriggerList",
timeTrigGVR: "TimeTriggerList",
}
dyn := dynamicfake.NewSimpleDynamicClientWithCustomListKinds(runtime.NewScheme(), listKinds, env)
s := &server{
dyn: dyn,
ns: "default",
routerURL: mockRouter.URL,
http: mockRouter.Client(),
authUser: "admin",
authPass: "pass",
}
// Create function first
createBody := `{"name":"inv-fn","environment":"python","code":"print(1)","route":"/inv-fn","methods":["GET"]}`
createRec := httptest.NewRecorder()
s.handleCreateFunction(createRec, httptest.NewRequest(http.MethodPost, "/api/functions", strings.NewReader(createBody)))
if createRec.Code != http.StatusCreated {
t.Fatalf("create: %d %s", createRec.Code, createRec.Body.String())
}
// Invoke
invokeRec := httptest.NewRecorder()
s.handleInvokeFunction(invokeRec, httptest.NewRequest(http.MethodPost, "/api/functions/inv-fn/invoke", strings.NewReader(`{}`)), "inv-fn")
if invokeRec.Code != http.StatusOK {
t.Fatalf("invoke: %d %s", invokeRec.Code, invokeRec.Body.String())
}
// Verify JWT was obtained via login and sent
if gotAuth != "Bearer fake-jwt-token-xyz" {
t.Fatalf("expected 'Bearer fake-jwt-token-xyz', got %q", gotAuth)
}
var out map[string]any
if err := json.Unmarshal(invokeRec.Body.Bytes(), &out); err != nil {
t.Fatalf("decode: %v", err)
}
if out["status"] != float64(200) {
t.Fatalf("expected status 200, got %v", out["status"])
}
if !strings.Contains(out["response_raw"].(string), "hello") {
t.Fatalf("unexpected response: %v", out["response_raw"])
}
}