fix: RBAC bind cluster-admin + list namespaces + TEST_MODE multi-tenant test

This commit is contained in:
Naeel
2026-04-19 15:41:11 +03:00
parent 9ebd36eed3
commit 551a5b2f8e
2 changed files with 110 additions and 76 deletions
+103 -75
View File
@@ -59,6 +59,7 @@ type server struct {
http *http.Client
saTokenPath string
invokeTimeout time.Duration
testMode bool // FISSION_TEST_MODE=true — пропускает deck auth, X-Test-Sub задаёт user
authUser string
authPass string
@@ -129,6 +130,7 @@ func main() {
invokeTimeout: invokeTimeout,
authUser: authUser,
authPass: authPass,
testMode: os.Getenv("FISSION_TEST_MODE") == "true",
}
mux := http.NewServeMux()
@@ -154,24 +156,40 @@ func main() {
auth := func(h http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
token := strings.TrimSpace(r.Header.Get("X-Auth-Token"))
env := strings.TrimSpace(strings.ToLower(r.Header.Get("X-Auth-Env")))
if _, ok := deckAPIs[env]; !ok {
env = "test"
}
if token == "" {
writeJSONError(w, http.StatusUnauthorized, "unauthorized")
return
}
if err := s.validateDeckToken(token, env); err != nil {
writeJSONError(w, http.StatusUnauthorized, "unauthorized")
return
}
ns, err := namespaceFromJWT(token)
if err != nil {
log.Printf("namespaceFromJWT: %v", err)
ns = s.ns
var ns string
if s.testMode {
// TEST_MODE: deck API не вызывается.
// X-Test-Sub задаёт произвольный sub → разные namespace-ы для тестирования.
sub := strings.TrimSpace(r.Header.Get("X-Test-Sub"))
if sub == "" {
writeJSONError(w, http.StatusUnauthorized, "test mode: X-Test-Sub required")
return
}
h32 := sha256.Sum256([]byte(sub))
ns = "fission-" + hex.EncodeToString(h32[:8])
} else {
token := strings.TrimSpace(r.Header.Get("X-Auth-Token"))
env := strings.TrimSpace(strings.ToLower(r.Header.Get("X-Auth-Env")))
if _, ok := deckAPIs[env]; !ok {
env = "test"
}
if token == "" {
writeJSONError(w, http.StatusUnauthorized, "unauthorized")
return
}
if err := s.validateDeckToken(token, env); err != nil {
writeJSONError(w, http.StatusUnauthorized, "unauthorized")
return
}
var err error
ns, err = namespaceFromJWT(token)
if err != nil {
log.Printf("namespaceFromJWT: %v", err)
ns = s.ns
}
}
ctx := context.WithValue(r.Context(), ctxKeyNS{}, ns)
h(w, r.WithContext(ctx))
}
@@ -258,6 +276,16 @@ func (s *server) handleCreateFunction(w http.ResponseWriter, r *http.Request) {
return
}
// Гарантируем что namespace + RBAC созданы до любых операций с ресурсами.
// handleAuth делает это при логине, но в test mode или при прямом вызове API
// namespace может отсутствовать — создаём idempotent.
nsCtx, nsCancel := context.WithTimeout(r.Context(), 30*time.Second)
defer nsCancel()
if err := s.ensureUserNamespace(nsCtx, ns); err != nil {
writeJSONError(w, http.StatusBadGateway, fmt.Sprintf("ensure namespace: %v", err))
return
}
req.Name = strings.TrimSpace(req.Name)
req.Language = strings.TrimSpace(req.Language)
req.Environment = strings.TrimSpace(req.Environment)
@@ -427,11 +455,11 @@ func (s *server) handleCreateFunction(w http.ResponseWriter, r *http.Request) {
}
writeAnyJSON(w, http.StatusCreated, map[string]any{
"name": req.Name,
"package": pkgName,
"name": req.Name,
"package": pkgName,
"httptrigger": triggerName,
"route": req.Route,
"expires_at": fnAnnotations["fission-console/expires-at"],
"route": req.Route,
"expires_at": fnAnnotations["fission-console/expires-at"],
})
}
@@ -986,7 +1014,7 @@ func (s *server) addNSToFission(ctx context.Context, ns string) error {
return fmt.Errorf("get router deployment: %w", err)
}
currentVal := "default" // fallback если переменная не найдена
currentVal := "default" // fallback если переменная не найдена
containerName := "router" // имя контейнера нужно для StrategicMergePatch
containers, _, _ := unstructured.NestedSlice(routerDep.Object, "spec", "template", "spec", "containers")
for _, c := range containers {
@@ -1083,60 +1111,60 @@ func (s *server) ensureUserNamespace(ctx context.Context, ns string) error {
return fmt.Errorf("create namespace %s: %w", ns, err)
}
// 1b. RoleBindings для Fission SA в user namespace.
//
// Проблема: Fission компоненты (executor, router, buildermgr и др.) работают в namespace
// "fission", но при добавлении нового namespace в FISSION_RESOURCE_NAMESPACES они начинают
// туда смотреть (list/watch). По умолчанию у их SA нет прав в чужих namespace-ах →
// "forbidden: cannot list environments.fission.io in namespace X".
//
// Почему cluster-admin, а не admin:
// ClusterRole "admin" не включает custom resource группы (fission.io/*).
// Fission executor при старте пытается создать Role с правами на fission.io/packages,
// и получает "attempting to grant RBAC permissions not currently held" — RBAC escalation
// prevention. ClusterRole "cluster-admin" в контексте RoleBinding (не ClusterRoleBinding)
// даёт полный доступ ТОЛЬКО внутри конкретного namespace — это безопасно.
//
// Операция idempotent: если RoleBinding уже существует — IsAlreadyExists игнорируется.
fissionSAs := []string{"fission-executor", "fission-router", "fission-buildermgr", "fission-kubewatcher", "fission-timer", "fission-fetcher", "fission-builder"}
fissionSysNS := os.Getenv("FISSION_SYSTEM_NAMESPACE")
if fissionSysNS == "" {
fissionSysNS = "fission"
}
rbGVR := schema.GroupVersionResource{Group: "rbac.authorization.k8s.io", Version: "v1", Resource: "rolebindings"}
for _, sa := range fissionSAs {
rbObj := &unstructured.Unstructured{
Object: map[string]any{
"apiVersion": "rbac.authorization.k8s.io/v1",
"kind": "RoleBinding",
"metadata": map[string]any{
"name": "fission-" + sa + "-user-ns",
"namespace": ns,
},
"roleRef": map[string]any{
"apiGroup": "rbac.authorization.k8s.io",
"kind": "ClusterRole",
"name": "cluster-admin", // namespace-scoped через RoleBinding, не ClusterRoleBinding
},
"subjects": []any{
map[string]any{
"kind": "ServiceAccount",
"name": sa,
"namespace": fissionSysNS,
},
},
},
}
_, rbErr := s.dyn.Resource(rbGVR).Namespace(ns).Create(ctx, rbObj, metav1.CreateOptions{})
if rbErr != nil && !apierrors.IsAlreadyExists(rbErr) {
log.Printf("ensureUserNamespace: create rolebinding %s/%s: %v", ns, sa, rbErr)
}
}
// 1b. RoleBindings для Fission SA в user namespace.
//
// Проблема: Fission компоненты (executor, router, buildermgr и др.) работают в namespace
// "fission", но при добавлении нового namespace в FISSION_RESOURCE_NAMESPACES они начинают
// туда смотреть (list/watch). По умолчанию у их SA нет прав в чужих namespace-ах →
// "forbidden: cannot list environments.fission.io in namespace X".
//
// Почему cluster-admin, а не admin:
// ClusterRole "admin" не включает custom resource группы (fission.io/*).
// Fission executor при старте пытается создать Role с правами на fission.io/packages,
// и получает "attempting to grant RBAC permissions not currently held" — RBAC escalation
// prevention. ClusterRole "cluster-admin" в контексте RoleBinding (не ClusterRoleBinding)
// даёт полный доступ ТОЛЬКО внутри конкретного namespace — это безопасно.
//
// Операция idempotent: если RoleBinding уже существует — IsAlreadyExists игнорируется.
fissionSAs := []string{"fission-executor", "fission-router", "fission-buildermgr", "fission-kubewatcher", "fission-timer", "fission-fetcher", "fission-builder"}
fissionSysNS := os.Getenv("FISSION_SYSTEM_NAMESPACE")
if fissionSysNS == "" {
fissionSysNS = "fission"
}
rbGVR := schema.GroupVersionResource{Group: "rbac.authorization.k8s.io", Version: "v1", Resource: "rolebindings"}
for _, sa := range fissionSAs {
rbObj := &unstructured.Unstructured{
Object: map[string]any{
"apiVersion": "rbac.authorization.k8s.io/v1",
"kind": "RoleBinding",
"metadata": map[string]any{
"name": "fission-" + sa + "-user-ns",
"namespace": ns,
},
"roleRef": map[string]any{
"apiGroup": "rbac.authorization.k8s.io",
"kind": "ClusterRole",
"name": "cluster-admin", // namespace-scoped через RoleBinding, не ClusterRoleBinding
},
"subjects": []any{
map[string]any{
"kind": "ServiceAccount",
"name": sa,
"namespace": fissionSysNS,
},
},
},
}
_, rbErr := s.dyn.Resource(rbGVR).Namespace(ns).Create(ctx, rbObj, metav1.CreateOptions{})
if rbErr != nil && !apierrors.IsAlreadyExists(rbErr) {
log.Printf("ensureUserNamespace: create rolebinding %s/%s: %v", ns, sa, rbErr)
}
}
// 1c. Регистрируем новый namespace в Fission (FISSION_RESOURCE_NAMESPACES).
// Только при первом создании — повторный патч не нужен, Fission уже знает о namespace.
// addNSToFission читает текущее значение переменной у router-а, добавляет ns и патчит
// все Fission deployments (router, executor, buildermgr, kubewatcher, timer).
// 1c. Регистрируем новый namespace в Fission (FISSION_RESOURCE_NAMESPACES).
// Только при первом создании — повторный патч не нужен, Fission уже знает о namespace.
// addNSToFission читает текущее значение переменной у router-а, добавляет ns и патчит
// все Fission deployments (router, executor, buildermgr, kubewatcher, timer).
if newlyCreated {
if patchErr := s.addNSToFission(ctx, ns); patchErr != nil {
log.Printf("ensureUserNamespace: addNSToFission: %v", patchErr)
+7 -1
View File
@@ -5,13 +5,19 @@ metadata:
rules:
- apiGroups: [""]
resources: ["namespaces"]
verbs: ["get", "create"]
verbs: ["get", "create", "list"]
- apiGroups: ["fission.io"]
resources: ["*"]
verbs: ["*"]
- apiGroups: ["rbac.authorization.k8s.io"]
resources: ["rolebindings"]
verbs: ["get", "create"]
# bind позволяет создавать RoleBindings ссылающиеся на cluster-admin
# без необходимости самому иметь все его права (RBAC escalation prevention)
- apiGroups: ["rbac.authorization.k8s.io"]
resources: ["clusterroles"]
resourceNames: ["cluster-admin"]
verbs: ["bind"]
- apiGroups: ["apps"]
resources: ["deployments"]
verbs: ["get", "patch"]