Skip to content

Commit 571d761

Browse files
committed
feat(bff): introduce support of multiple auth methods (internal, user_token)
- Introduce auth-method flag (default: internal) to select auth strategy - internal: uses pod service account (in-cluster) or current kubeconfig (local) - user_token: uses bearer token from X-Forwarded-Access-Token header - Implement separate SAR (internal) and SSAR (user_token) logic - Clarify behavior and usage in README Signed-off-by: Eder Ignatowicz <[email protected]>
1 parent 0e7fbf6 commit 571d761

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

57 files changed

+2083
-1170
lines changed

clients/ui/bff/Makefile

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ MOCK_MR_CLIENT ?= false
44
DEV_MODE ?= false
55
DEV_MODE_PORT ?= 8080
66
STANDALONE_MODE ?= true
7+
AUTH_METHOD ?= internal
78
#frontend static assets root directory
89
STATIC_ASSETS_DIR ?= ./static
910
# ENVTEST_K8S_VERSION refers to the version of kubebuilder assets to be downloaded by envtest binary.
@@ -50,7 +51,7 @@ build: fmt vet test ## Builds the project to produce a binary executable.
5051
.PHONY: run
5152
run: fmt vet envtest ## Runs the project.
5253
ENVTEST_ASSETS="$(shell $(ENVTEST) use $(ENVTEST_K8S_VERSION) --bin-dir $(LOCALBIN) -p path)" \
53-
go run ./cmd --port=$(PORT) --static-assets-dir=$(STATIC_ASSETS_DIR) --mock-k8s-client=$(MOCK_K8S_CLIENT) --mock-mr-client=$(MOCK_MR_CLIENT) --dev-mode=$(DEV_MODE) --dev-mode-port=$(DEV_MODE_PORT) --standalone-mode=$(STANDALONE_MODE) --log-level=$(LOG_LEVEL) --allowed-origins=$(ALLOWED_ORIGINS)
54+
go run ./cmd --port=$(PORT) --auth-method=${AUTH_METHOD} --static-assets-dir=$(STATIC_ASSETS_DIR) --mock-k8s-client=$(MOCK_K8S_CLIENT) --mock-mr-client=$(MOCK_MR_CLIENT) --dev-mode=$(DEV_MODE) --dev-mode-port=$(DEV_MODE_PORT) --standalone-mode=$(STANDALONE_MODE) --log-level=$(LOG_LEVEL) --allowed-origins=$(ALLOWED_ORIGINS)
5455

5556
##@ Dependencies
5657

clients/ui/bff/README.md

Lines changed: 35 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ The main difference between the two docker compose files is that `-local` one bu
2424

2525
When shutting down the docker compose, you might want to clean-up the SQLite db file generated by ML Metadata, for example `./test/config/ml-metadata/metadata.sqlite.db`
2626

27-
# Development
27+
## Development
2828

2929
Run the following command to build the BFF:
3030

@@ -51,7 +51,7 @@ If you want to change the log level on deployment, add the LOG_LEVEL argument wh
5151
make run LOG_LEVEL=DEBUG
5252
```
5353

54-
# Building and Deploying
54+
## Building and Deploying
5555

5656
Run the following command to build the BFF:
5757

@@ -78,25 +78,25 @@ See the [OpenAPI specification](../api/openapi/mod-arch.yaml) for a complete lis
7878
You will need to inject your requests with a `kubeflow-userid` header and namespace for authorization purposes.
7979

8080
When running the service with the mocked Kubernetes client (MOCK_K8S_CLIENT=true), the user `[email protected]` is preconfigured with the necessary RBAC permissions to perform these actions.
81-
82-
```
83-
# GET /v1/healthcheck
84-
curl -i -H "kubeflow-userid: [email protected]" "localhost:4000/api/v1/healthcheck"
8581
```
86-
82+
# GET /v1/healthcheck
83+
curl -i "localhost:4000/healthcheck"
84+
```
8785
```
88-
# GET /v1/user
86+
# GET /v1/user
8987
curl -i -H "kubeflow-userid: [email protected]" "localhost:4000/api/v1/user"
88+
curl -i -H "X-Forwarded-Access-Token: $TOKEN" "localhost:4000/api/v1/user"
9089
```
91-
9290
```
9391
# GET /v1/namespaces (only works when DEV_MODE=true)
9492
curl -i -H "kubeflow-userid: [email protected]" "localhost:4000/api/v1/namespaces"
93+
curl -i -H "X-Forwarded-Access-Token: $TOKEN" "localhost:4000/api/v1/namespaces"
9594
```
9695

9796
```
9897
# GET /v1/model_registry
9998
curl -i -H "kubeflow-userid: [email protected]" "localhost:4000/api/v1/model_registry?namespace=kubeflow"
99+
curl -i -H "X-Forwarded-Access-Token: $TOKEN" "localhost:4000/api/v1/model_registry?namespace=kubeflow"
100100
```
101101

102102
```
@@ -110,6 +110,7 @@ curl -i \
110110
```
111111
# GET /v1/model_registry/{model_registry_id}/registered_models
112112
curl -i -H "kubeflow-userid: [email protected]" "localhost:4000/api/v1/model_registry/model-registry/registered_models?namespace=kubeflow"
113+
curl -i -H "X-Forwarded-Access-Token: $TOKEN" "localhost:4000/api/v1/model_registry/model-registry-service/registered_models?namespace=kubeflow-user-example-com""
113114
```
114115

115116
```
@@ -350,24 +351,38 @@ The mock Kubernetes environment is activated when the environment variable `MOCK
350351
- `non-model-registry`: resides in the `kubeflow` namespace _without_ the label `component: model-registry`.
351352
- `model-registry-dora`: resides in the `dora-namespace` namespace with the label `component: model-registry`.
352353

353-
#### 3. How BFF authorization works for kubeflow-userid and kubeflow-groups?
354+
#### 3. Which Authentication methods are supported?
355+
356+
The BFF supports two authentication modes, selectable via the --auth-method flag or AUTH_METHOD environment variable (default: internal):
354357

355-
Authorization is performed using Kubernetes SubjectAccessReview (SAR), which validates user access to resources.
358+
- `internal`: Uses the credentials of the running backend.
359+
- If running inside the cluster, it uses the pod’s service account.
360+
- If running locally (e.g. for development), it uses the current user's active kubeconfig context.
361+
- In this mode, user identity is passed via the kubeflow-userid and optionally kubeflow-groups headers.
362+
- This is the default mode and works well with mock clients and local testing.
363+
- `user_token`: Uses a user-provided Bearer token for authentication.
364+
- The token must be passed in the `X-Forwarded-Access-Token` header.
365+
- Useful when the frontend is fronted by an auth proxy (e.g., Istio + OIDC) that injects a valid Kubernetes token.
356366

357-
- `kubeflow-userid`: Required header that specifies the user’s email. Access is checked directly for the user via SAR.
358-
- `kubeflow-groups`: Optional header with a comma-separated list of groups. If the user does not have access, SAR checks group permissions using OR logic. If any group has access, the request is authorized.
359367

360-
Access to Model Registry List:
368+
#### 4. How BFF authorization works?
361369

362-
- To list all model registries (/v1/model_registry), we perform a SAR check for get and list verbs on services within the specified namespace.
363-
- If the user or any group has permission to get and list services in the namespace, the request is authorized.
370+
Authorization is performed using Kubernetes access reviews, validating whether the user (or their groups) can perform certain actions.
371+
There are two review mechanisms depending on the authentication mode:
372+
- Internal mode (auth-method=internal):
373+
Uses SubjectAccessReview (SAR) to check whether the impersonated user (from kubeflow-userid and kubeflow-groups headers) has the required permissions.
374+
- User token mode (auth-method=user_token): Uses SelfSubjectAccessReview (SSAR), leveraging the Bearer token provided in the X-Forwarded-Access-Token header to check the current user's permissions directly.
364375

365-
Access to Specific Model Registry Endpoints:
376+
##### Authorization logic
377+
* Access to Model Registry List (/v1/model_registry):
378+
- Checks for get and list on services in the target namespace.
379+
- If the user (or groups, in internal mode) has permission, access is granted.
366380

367-
- For other endpoints (e.g., /v1/model_registry/{model_registry_id}/...), we perform a SAR check for get and list verbs on the specific service (identified by model_registry_id) within the namespace.
368-
- If the user or any group has permission to get or list the specific service, the request is authorized.
381+
* Access to Specific Model Registry Endpoints (/v1/model_registry/{model_registry_id}/...):
382+
- Checks for get on the specific service (identified by model_registry_id) in the namespace.
383+
- If authorized, access is granted.
369384

370-
#### 4. How do I allow CORS requests from other origins
385+
#### 5. How do I allow CORS requests from other origins
371386

372387
When serving the UI directly from the BFF there is no need for any CORS headers to be served, by default they are turned off for security reasons.
373388

clients/ui/bff/cmd/main.go

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ import (
2020
func main() {
2121
var cfg config.EnvConfig
2222
var certFile, keyFile string
23-
23+
fmt.Println("Starting Model Registry UI BFF...")
2424
flag.IntVar(&cfg.Port, "port", getEnvAsInt("PORT", 8080), "API server port")
2525
flag.StringVar(&certFile, "cert-file", "", "Path to TLS certificate file")
2626
flag.StringVar(&keyFile, "key-file", "", "Path to TLS key file")
@@ -32,12 +32,19 @@ func main() {
3232
flag.StringVar(&cfg.StaticAssetsDir, "static-assets-dir", "./static", "Configure frontend static assets root directory")
3333
flag.TextVar(&cfg.LogLevel, "log-level", parseLevel(getEnvAsString("LOG_LEVEL", "INFO")), "Sets server log level, possible values: error, warn, info, debug")
3434
flag.Func("allowed-origins", "Sets allowed origins for CORS purposes, accepts a comma separated list of origins or * to allow all, default none", newOriginParser(&cfg.AllowedOrigins, getEnvAsString("ALLOWED_ORIGINS", "")))
35+
flag.StringVar(&cfg.AuthMethod, "auth-method", "internal", "Authentication method (internal or user_token)")
3536
flag.Parse()
3637

3738
logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{
3839
Level: cfg.LogLevel,
3940
}))
4041

42+
//validate auth method
43+
if cfg.AuthMethod != config.AuthMethodInternal && cfg.AuthMethod != config.AuthMethodUser {
44+
logger.Error("invalid auth method: (must be internal or user_token)", "authMethod", cfg.AuthMethod)
45+
os.Exit(1)
46+
}
47+
4148
// Only use for logging errors about logging configuration.
4249
slog.SetDefault(logger)
4350

@@ -92,8 +99,8 @@ func main() {
9299
logger.Error("server shutdown failed", "error", err)
93100
}
94101

95-
// Shutdown the Kubernetes manager gracefully
96-
if err := app.Shutdown(ctx, logger); err != nil {
102+
// Shutdown the App gracefully
103+
if err := app.Shutdown(); err != nil {
97104
logger.Error("failed to shutdown Kubernetes manager", "error", err)
98105
}
99106

clients/ui/bff/go.mod

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,9 @@ require (
1818
)
1919

2020
require (
21-
github.com/beorn7/perks v1.0.1 // indirect
22-
github.com/cespare/xxhash/v2 v2.3.0 // indirect
2321
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
2422
github.com/emicklei/go-restful/v3 v3.11.0 // indirect
2523
github.com/evanphx/json-patch/v5 v5.9.0 // indirect
26-
github.com/fsnotify/fsnotify v1.7.0 // indirect
2724
github.com/fxamacker/cbor/v2 v2.7.0 // indirect
2825
github.com/go-logr/logr v1.4.2 // indirect
2926
github.com/go-logr/zapr v1.3.0 // indirect
@@ -32,7 +29,6 @@ require (
3229
github.com/go-openapi/swag v0.23.0 // indirect
3330
github.com/go-task/slim-sprig/v3 v3.0.0 // indirect
3431
github.com/gogo/protobuf v1.3.2 // indirect
35-
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
3632
github.com/golang/protobuf v1.5.4 // indirect
3733
github.com/google/gnostic-models v0.6.8 // indirect
3834
github.com/google/go-cmp v0.6.0 // indirect
@@ -47,10 +43,6 @@ require (
4743
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
4844
github.com/pkg/errors v0.9.1 // indirect
4945
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
50-
github.com/prometheus/client_golang v1.19.1 // indirect
51-
github.com/prometheus/client_model v0.6.1 // indirect
52-
github.com/prometheus/common v0.55.0 // indirect
53-
github.com/prometheus/procfs v0.15.1 // indirect
5446
github.com/spf13/pflag v1.0.5 // indirect
5547
github.com/stretchr/objx v0.5.2 // indirect
5648
github.com/x448/float16 v0.8.4 // indirect
@@ -64,7 +56,6 @@ require (
6456
golang.org/x/text v0.21.0 // indirect
6557
golang.org/x/time v0.7.0 // indirect
6658
golang.org/x/tools v0.26.0 // indirect
67-
gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect
6859
google.golang.org/protobuf v1.35.1 // indirect
6960
gopkg.in/inf.v0 v0.9.1 // indirect
7061
gopkg.in/yaml.v3 v3.0.1 // indirect

clients/ui/bff/go.sum

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,6 @@ github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1
1111
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
1212
github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g=
1313
github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc=
14-
github.com/evanphx/json-patch v0.5.2 h1:xVCHIVMUu1wtM/VkR9jVZ45N3FhZfYMMYGorLCR8P3k=
15-
github.com/evanphx/json-patch v0.5.2/go.mod h1:ZWS5hhDbVDyob71nXKNL0+PWn6ToqBHMikGIFbs31qQ=
1614
github.com/evanphx/json-patch/v5 v5.9.0 h1:kcBlZQbplgElYIlo/n1hJbls2z/1awpXxpRi0/FOJfg=
1715
github.com/evanphx/json-patch/v5 v5.9.0/go.mod h1:VNkHZ/282BpEyt/tObQO8s5CMPmYYq14uClGH4abBuQ=
1816
github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=

0 commit comments

Comments
 (0)