fix: invoke auth via Fission router JWT login
This commit is contained in:
@@ -46,7 +46,7 @@ spec:
|
|||||||
serviceAccountName: fission-console
|
serviceAccountName: fission-console
|
||||||
containers:
|
containers:
|
||||||
- name: console
|
- name: console
|
||||||
image: naeel/fission-console:v0.1.5
|
image: naeel/fission-console:v0.2.2
|
||||||
ports:
|
ports:
|
||||||
- containerPort: 8090
|
- containerPort: 8090
|
||||||
env:
|
env:
|
||||||
@@ -56,6 +56,16 @@ spec:
|
|||||||
value: "http://router.fission.svc.cluster.local"
|
value: "http://router.fission.svc.cluster.local"
|
||||||
- name: PORT
|
- name: PORT
|
||||||
value: "8090"
|
value: "8090"
|
||||||
|
- name: FISSION_AUTH_USERNAME
|
||||||
|
valueFrom:
|
||||||
|
secretKeyRef:
|
||||||
|
name: router
|
||||||
|
key: username
|
||||||
|
- name: FISSION_AUTH_PASSWORD
|
||||||
|
valueFrom:
|
||||||
|
secretKeyRef:
|
||||||
|
name: router
|
||||||
|
key: password
|
||||||
livenessProbe:
|
livenessProbe:
|
||||||
httpGet:
|
httpGet:
|
||||||
path: /health
|
path: /health
|
||||||
|
|||||||
+81
-8
@@ -11,6 +11,7 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"fission-console/ui"
|
"fission-console/ui"
|
||||||
@@ -32,11 +33,21 @@ var (
|
|||||||
timeTrigGVR = schema.GroupVersionResource{Group: "fission.io", Version: "v1", Resource: "timetriggers"}
|
timeTrigGVR = schema.GroupVersionResource{Group: "fission.io", Version: "v1", Resource: "timetriggers"}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const defaultSATokenPath = "/var/run/secrets/kubernetes.io/serviceaccount/token"
|
||||||
|
|
||||||
type server struct {
|
type server struct {
|
||||||
dyn dynamic.Interface
|
dyn dynamic.Interface
|
||||||
ns string
|
ns string
|
||||||
routerURL string
|
routerURL string
|
||||||
http *http.Client
|
http *http.Client
|
||||||
|
saTokenPath string
|
||||||
|
|
||||||
|
authUser string
|
||||||
|
authPass string
|
||||||
|
|
||||||
|
tokenMu sync.Mutex
|
||||||
|
cachedJWT string
|
||||||
|
tokenExpAt time.Time
|
||||||
}
|
}
|
||||||
|
|
||||||
type createFunctionRequest struct {
|
type createFunctionRequest struct {
|
||||||
@@ -68,11 +79,18 @@ func main() {
|
|||||||
log.Fatalf("create dynamic client: %v", err)
|
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{
|
s := &server{
|
||||||
dyn: dyn,
|
dyn: dyn,
|
||||||
ns: namespace,
|
ns: namespace,
|
||||||
routerURL: routerURL,
|
routerURL: routerURL,
|
||||||
http: &http.Client{Timeout: 30 * time.Second},
|
http: &http.Client{Timeout: 30 * time.Second},
|
||||||
|
saTokenPath: saTokenPath,
|
||||||
|
authUser: authUser,
|
||||||
|
authPass: authPass,
|
||||||
}
|
}
|
||||||
|
|
||||||
mux := http.NewServeMux()
|
mux := http.NewServeMux()
|
||||||
@@ -474,6 +492,9 @@ func (s *server) handleInvokeFunction(w http.ResponseWriter, r *http.Request, na
|
|||||||
if invokeMethod == http.MethodPost {
|
if invokeMethod == http.MethodPost {
|
||||||
req.Header.Set("Content-Type", "application/json")
|
req.Header.Set("Content-Type", "application/json")
|
||||||
}
|
}
|
||||||
|
if token := s.getRouterToken(); token != "" {
|
||||||
|
req.Header.Set("Authorization", "Bearer "+token)
|
||||||
|
}
|
||||||
|
|
||||||
resp, err := s.http.Do(req)
|
resp, err := s.http.Do(req)
|
||||||
if err != nil {
|
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) {
|
func (s *server) handleDeleteFunction(w http.ResponseWriter, r *http.Request, name string) {
|
||||||
ctx, cancel := context.WithTimeout(r.Context(), 20*time.Second)
|
ctx, cancel := context.WithTimeout(r.Context(), 20*time.Second)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|||||||
+72
-2
@@ -26,7 +26,7 @@ func newTestServer(objs ...runtime.Object) *server {
|
|||||||
timeTrigGVR: "TimeTriggerList",
|
timeTrigGVR: "TimeTriggerList",
|
||||||
}
|
}
|
||||||
dyn := dynamicfake.NewSimpleDynamicClientWithCustomListKinds(runtime.NewScheme(), listKinds, objs...)
|
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) {
|
func TestNormalizeMethods(t *testing.T) {
|
||||||
@@ -109,7 +109,7 @@ func TestUpdateFunctionCode(t *testing.T) {
|
|||||||
env := &unstructured.Unstructured{Object: map[string]any{
|
env := &unstructured.Unstructured{Object: map[string]any{
|
||||||
"apiVersion": "fission.io/v1",
|
"apiVersion": "fission.io/v1",
|
||||||
"kind": "Environment",
|
"kind": "Environment",
|
||||||
"metadata": map[string]any{"name": "python", "namespace": "default"},
|
"metadata": map[string]any{"name": "python", "namespace": "default"},
|
||||||
}}
|
}}
|
||||||
s := newTestServer(env)
|
s := newTestServer(env)
|
||||||
|
|
||||||
@@ -141,3 +141,73 @@ func TestUpdateFunctionCode(t *testing.T) {
|
|||||||
t.Fatalf("expected new-code, got %q", string(decoded))
|
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"])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user