Skip to content

Commit

Permalink
Add authorization mechanisms in new Katib UI backend (#1983)
Browse files Browse the repository at this point in the history
* UI(back): Add authorization mechanisms in new Katib UI backend

* Introduce helper ENV vars and functions for authentication and
  authorization checks. The authz checks are using SubjectAcessReviews
  objects.
  * BACKEND_MODE={dev,prod}: skip authz when in dev mode
  * APP_DISABLE_AUTH={bool}: skip authz if explicity requested

* Introduce a client-go client to construct SubjectAccessReview objects.

* Before any request proceed to K8s api-server:
  * check if authorization must be skipped (BACKEND_MODE, APP_DISBLE_AUTH)
  * check if a username is proviced in request Header
  * query the K8s api-server with SAR to ensure that the user has
    appropriate access privilleges

* Replace the /katib/fetch_experiment/ route with /katib/fetch_namespaces_experiments.
  This route expects a namespace as a query parameter from which all experiments will be fetched.

Signed-off-by: Apostolos Gerakaris <[email protected]>

* UI(front): Provide a namespace as a query parameter

This is needed for the new /katib/fetch_namespaced_experiments route.

Signed-off-by: Apostolos Gerakaris <[email protected]>

* Update README for running locally without auth

Update the README of the web app to expose that devs should set
APP_DISABLE_AUTH=true when running locally, since there's no authnz when
running locally.

Signed-off-by: Apostolos Gerakaris <[email protected]>

* remove duplicated variable types

Signed-off-by: Apostolos Gerakaris <[email protected]>

* Review fixes

* proper error handling.
* switch to Go's build-in errors package.
* set appropriate verbs when constructing SAR objects.

Signed-off-by: Apostolos Gerakaris <[email protected]>

* review: Use controller-runtime client to create SAR objects

Signed-off-by: Apostolos Gerakaris <[email protected]>

* Review fixes

* fix backend routes.
  * '/katib/fetch_namespaces' to fetch experiments in a namespace
  * 'FetchExperiments' handler

* hit the appropriate route from frontend and provide namespace as a
  query parameter  to fetch experiments

* remove remove BACKEND_MODE env var in
  favour of the more specific APP_DISABLE_AUTH

Signed-off-by: Apostolos Gerakaris <[email protected]>

* Review fixes

* Add constants for CRUD actions
* Add plural for experiments and suggestions as constants
* Add GetUsername logic under IsAuthorized and handle errors properly
* Have APP_DISABLE_AUTH by default as true, since currently Katib
  doesn't support this feature in standalone mode.

Signed-off-by: Apostolos Gerakaris <[email protected]>

Signed-off-by: Apostolos Gerakaris <[email protected]>
  • Loading branch information
apo-ger authored Nov 28, 2022
1 parent 0a1cb31 commit 831e1d3
Show file tree
Hide file tree
Showing 9 changed files with 403 additions and 42 deletions.
2 changes: 1 addition & 1 deletion cmd/new-ui/v1beta1/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ func main() {
http.HandleFunc("/katib/", kuh.ServeIndex(*buildDir))
http.Handle("/katib/static/", http.StripPrefix("/katib/", frontend))

http.HandleFunc("/katib/fetch_experiments/", kuh.FetchAllExperiments)
http.HandleFunc("/katib/fetch_experiments/", kuh.FetchExperiments)

http.HandleFunc("/katib/create_experiment/", kuh.CreateExperiment)

Expand Down
15 changes: 15 additions & 0 deletions pkg/controller.v1beta1/consts/const.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,23 @@ import (

const (

// ActionTypeCreate is the create CRUD action
ActionTypeCreate = "create"
// ActionTypeList is the list CRUD action
ActionTypeList = "list"
// ActionTypeGet is the get CRUD action
ActionTypeGet = "get"
// ActionTypeUpdate is the update CRUD action
ActionTypeUpdate = "update"
// ActionTypeDelete is the delete CRUD action
ActionTypeDelete = "delete"

// PluralTrial is the plural for Trial object
PluralTrial = "trials"
// PluralExperiment is the plural for Experiment object
PluralExperiment = "experiments"
// PluralSuggestion is the plural for Suggestion object
PluralSuggestion = "suggestions"

// ConfigExperimentSuggestionName is the config name of the
// suggestion client implementation in experiment controller.
Expand Down
1 change: 1 addition & 0 deletions pkg/new-ui/v1beta1/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ This is the recommended way to test the web app e2e. In order to build the UI an
For example, if you use port-forwarding to expose `katib-db-manager`, run this command:

```
export APP_DISABLE_AUTH=true
go run main.go --build-dir=../../../pkg/new-ui/v1beta1/frontend/dist --port=8080 --db-manager-address=localhost:6789
```

Expand Down
95 changes: 95 additions & 0 deletions pkg/new-ui/v1beta1/authzn.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
package v1beta1

import (
"context"
"errors"
"fmt"
"log"
"net/http"
"strings"

"github.com/kubeflow/katib/pkg/util/v1beta1/env"
v1 "k8s.io/api/authorization/v1"
"k8s.io/apimachinery/pkg/runtime/schema"
"sigs.k8s.io/controller-runtime/pkg/client"
)

// ENV variables
var (
USER_HEADER = env.GetEnvOrDefault("USERID_HEADER", "kubeflow-userid")
USER_PREFIX = env.GetEnvOrDefault("USERID_PREFIX", ":")
DISABLE_AUTH = env.GetEnvOrDefault("APP_DISABLE_AUTH", "true")
)

// Function for constructing SubjectAccessReviews (SAR) objects
func CreateSAR(user, verb, namespace, resource, subresource, name string, schema schema.GroupVersion) *v1.SubjectAccessReview {

sar := &v1.SubjectAccessReview{
Spec: v1.SubjectAccessReviewSpec{
User: user,
ResourceAttributes: &v1.ResourceAttributes{
Namespace: namespace,
Verb: verb,
Group: schema.Group,
Version: schema.Version,
Resource: resource,
Subresource: subresource,
Name: name,
},
},
}
return sar
}

func IsAuthorized(verb, namespace, resource, subresource, name string, schema schema.GroupVersion, client client.Client, r *http.Request) (string, error) {

// We disable authn/authz checks when in standalone mode.
if DISABLE_AUTH == "true" {
log.Printf("APP_DISABLE_AUTH set to True. Skipping authentication/authorization checks")
return "", nil
}
// Check if an incoming request is from an authenticated user (kubeflow mode: kubeflow-userid header)
if r.Header.Get(USER_HEADER) == "" {
return "", errors.New("user header not present")
}
user := r.Header.Get(USER_HEADER)
user = strings.Replace(user, USER_PREFIX, "", 1)

// Check if the user is authorized to perform a given action on katib/k8s resources.
sar := CreateSAR(user, verb, namespace, resource, subresource, name, schema)
err := client.Create(context.TODO(), sar)
if err != nil {
log.Printf("Error submitting SubjectAccessReview: %v, %s", sar, err.Error())
return user, err
}

if sar.Status.Allowed {
return user, nil
}

msg := generateUnauthorizedMessage(user, verb, namespace, resource, subresource, schema, sar)
return user, errors.New(msg)
}

func generateUnauthorizedMessage(user, verb, namespace, resource, subresource string, schema schema.GroupVersion, sar *v1.SubjectAccessReview) string {

msg := fmt.Sprintf("User: %s is not authorized to %s", user, verb)

if schema.Group == "" {
msg += fmt.Sprintf(" %s/%s", schema.Version, resource)
} else {
msg += fmt.Sprintf(" %s/%s/%s", schema.Group, schema.Version, resource)
}

if subresource != "" {
msg += fmt.Sprintf("/%s", subresource)
}

if namespace != "" {
msg += fmt.Sprintf(" in namespace: %s", namespace)
}
if sar.Status.Reason != "" {
msg += fmt.Sprintf(" ,reason: %s", sar.Status.Reason)
}
return msg
}
Loading

0 comments on commit 831e1d3

Please sign in to comment.