Skip to content

Commit

Permalink
NETOBSERV-870 implement TokenReview (#283)
Browse files Browse the repository at this point in the history
* NETOBSERV-870 implement TokenReview

Requires operator PR to grant permission for TokenReviews

- 3 auth modes: check for cluster-admin, check for any user, no check
  (insecure; only for debugging/dev mode)
- add tests
- fix broken dev mode

* Merge check implementations

* Add 'auto' mode

Auto mode acts either as admin or authenticated depending on the loki
authtoken mode (forward => authenticated)

Also improve loki errors hanfling
  • Loading branch information
jotak authored Mar 1, 2023
1 parent 5101014 commit 04ae8b3
Show file tree
Hide file tree
Showing 18 changed files with 415 additions and 1,621 deletions.
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ GOLANGCI_LINT_VERSION = v1.50.1
COVERPROFILE = coverage.out
NPM_INSTALL ?= install

CMDLINE_ARGS ?= --loglevel trace --loki-tenant-id netobserv --frontend-config config/sample-frontend-config.yaml
CMDLINE_ARGS ?= --loglevel trace --loki-tenant-id netobserv --frontend-config config/sample-frontend-config.yaml --auth-check none

ifeq (,$(shell which podman 2>/dev/null))
OCI_BIN ?= docker
Expand Down
25 changes: 23 additions & 2 deletions cmd/plugin-backend.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ import (

"github.com/sirupsen/logrus"

"github.com/netobserv/network-observability-console-plugin/pkg/handler/auth"
"github.com/netobserv/network-observability-console-plugin/pkg/kubernetes/auth"
"github.com/netobserv/network-observability-console-plugin/pkg/kubernetes/client"
"github.com/netobserv/network-observability-console-plugin/pkg/loki"
"github.com/netobserv/network-observability-console-plugin/pkg/server"
)
Expand Down Expand Up @@ -39,6 +40,7 @@ var (
lokiMock = flag.Bool("loki-mock", false, "Fake loki results using saved mocks")
logLevel = flag.String("loglevel", "info", "log level (default: info)")
frontendConfig = flag.String("frontend-config", "", "path to the console plugin config file")
authCheck = flag.String("auth-check", "auto", "type of authentication check: authenticated, admin, auto or none (default is auto, based on loki auth mode)")
versionFlag = flag.Bool("v", false, "print version")
log = logrus.WithField("module", "main")
)
Expand Down Expand Up @@ -81,6 +83,25 @@ func main() {
log.Fatal("labels cannot be empty")
}

var checkType auth.CheckType
if *authCheck == "auto" {
if *lokiForwardUserToken {
checkType = auth.CheckAuthenticated
} else {
checkType = auth.CheckAdmin
}
log.Info(fmt.Sprintf("auth-check 'auto' resolved to '%s'", checkType))
} else {
checkType = auth.CheckType(*authCheck)
}
if checkType == auth.CheckNone {
log.Warn("INSECURE: auth checker is disabled")
}
checker, err := auth.NewChecker(checkType, client.NewInCluster)
if err != nil {
log.WithError(err).Fatal("auth checker error")
}

server.Start(&server.Config{
Port: *port,
CertFile: *cert,
Expand All @@ -91,5 +112,5 @@ func main() {
CORSMaxAge: *corsMaxAge,
Loki: loki.NewConfig(lURL, lStatusURL, *lokiTimeout, *lokiTenantID, *lokiTokenPath, *lokiForwardUserToken, *lokiSkipTLS, *lokiCAPath, *lokiMock, strings.Split(lLabels, ",")),
FrontendConfig: *frontendConfig,
}, &auth.BearerTokenChecker{})
}, checker)
}
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ require (
github.com/sirupsen/logrus v1.8.1
github.com/stretchr/testify v1.8.0
gopkg.in/yaml.v3 v3.0.1
k8s.io/api v0.26.1
k8s.io/apimachinery v0.26.1
k8s.io/client-go v0.26.1
)
Expand Down Expand Up @@ -48,7 +49,6 @@ require (
google.golang.org/protobuf v1.28.1 // indirect
gopkg.in/inf.v0 v0.9.1 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
k8s.io/api v0.26.1 // indirect
k8s.io/klog/v2 v2.80.1 // indirect
k8s.io/kube-openapi v0.0.0-20221012153701-172d655c2280 // indirect
k8s.io/utils v0.0.0-20221107191617-1a15be271d1d // indirect
Expand Down
66 changes: 0 additions & 66 deletions pkg/handler/auth/check_auth.go

This file was deleted.

23 changes: 12 additions & 11 deletions pkg/handler/loki.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package handler

import (
"encoding/json"
"errors"
"fmt"
"net/http"
"strings"
Expand All @@ -12,9 +11,9 @@ import (
"github.com/sirupsen/logrus"
"gopkg.in/yaml.v3"

"github.com/netobserv/network-observability-console-plugin/pkg/handler/auth"
"github.com/netobserv/network-observability-console-plugin/pkg/handler/lokiclientmock"
"github.com/netobserv/network-observability-console-plugin/pkg/httpclient"
"github.com/netobserv/network-observability-console-plugin/pkg/kubernetes/auth"
"github.com/netobserv/network-observability-console-plugin/pkg/loki"
"github.com/netobserv/network-observability-console-plugin/pkg/metrics"
"github.com/netobserv/network-observability-console-plugin/pkg/model"
Expand Down Expand Up @@ -66,21 +65,24 @@ func EncodeQuery(url string) string {
return unspaced
}

func getLokiError(resp []byte, code int) string {
func getLokiError(resp []byte, code int) (int, string) {
var f map[string]string
if code == http.StatusBadRequest {
return fmt.Sprintf("Loki message: %s", resp)
return code, fmt.Sprintf("Loki message: %s", resp)
}
if code == http.StatusForbidden {
return code, fmt.Sprintf("Forbidden: %s", resp)
}
err := json.Unmarshal(resp, &f)
if err != nil {
hlog.WithError(err).Errorf("cannot unmarshal, response was: %v", string(resp))
return fmt.Sprintf("Unknown error from Loki\ncannot unmarshal\n%s", resp)
return http.StatusBadRequest, fmt.Sprintf("Unknown error from Loki\ncannot unmarshal\n%s", resp)
}
message, ok := f["message"]
if !ok {
return "Unknown error from Loki\nno message found"
return http.StatusBadRequest, "Unknown error from Loki\nno message found"
}
return fmt.Sprintf("Loki message: %s", message)
return http.StatusBadRequest, fmt.Sprintf("Loki message: %s", message)
}

func executeLokiQuery(flowsURL string, lokiClient httpclient.Caller) ([]byte, int, error) {
Expand All @@ -96,11 +98,10 @@ func executeLokiQuery(flowsURL string, lokiClient httpclient.Caller) ([]byte, in
return nil, http.StatusServiceUnavailable, err
}
if code != http.StatusOK {
msg := getLokiError(resp, code)
return nil, http.StatusBadRequest, errors.New(msg)
newCode, msg := getLokiError(resp, code)
return nil, newCode, fmt.Errorf("[%d] %s", code, msg)
}
code = http.StatusOK
return resp, code, nil
return resp, http.StatusOK, nil
}

func fetchSingle(lokiClient httpclient.Caller, flowsURL string, merger loki.Merger) (int, error) {
Expand Down
4 changes: 2 additions & 2 deletions pkg/handler/resources.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,8 +61,8 @@ func getLabelValues(cfg *loki.Config, lokiClient httpclient.Caller, label string
return nil, http.StatusServiceUnavailable, err
}
if code != http.StatusOK {
msg := getLokiError(resp, code)
return nil, http.StatusBadRequest, errors.New(msg)
newCode, msg := getLokiError(resp, code)
return nil, newCode, errors.New(msg)
}
hlog.Tracef("GetFlows raw response: %s", resp)
var lvr model.LabelValuesResponse
Expand Down
123 changes: 123 additions & 0 deletions pkg/kubernetes/auth/check_auth.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
package auth

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

"github.com/netobserv/network-observability-console-plugin/pkg/kubernetes/client"
"github.com/sirupsen/logrus"
authv1 "k8s.io/api/authentication/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

var hlog = logrus.WithField("module", "handler.auth")

type CheckType string

const (
AuthHeader = "Authorization"
CheckAuthenticated CheckType = "authenticated"
CheckAdmin CheckType = "admin"
CheckNone CheckType = "none"
)

type Checker interface {
CheckAuth(ctx context.Context, header http.Header) error
}

func NewChecker(typez CheckType, apiProvider client.APIProvider) (Checker, error) {
switch typez {
case CheckNone:
return &NoopChecker{}, nil
case CheckAuthenticated:
return &BearerTokenChecker{apiProvider: apiProvider, predicates: []tokenReviewPredicate{mustBeAuthenticated}}, nil
case CheckAdmin:
return &BearerTokenChecker{apiProvider: apiProvider, predicates: []tokenReviewPredicate{mustBeAuthenticated, mustBeClusterAdmin}}, nil
}
return nil, fmt.Errorf("auth checker type unknown: %s. Must be one of %s, %s, %s", typez, CheckAdmin, CheckAuthenticated, CheckNone)
}

type NoopChecker struct {
Checker
}

func (b *NoopChecker) CheckAuth(ctx context.Context, header http.Header) error {
hlog.Debug("noop auth checker: ignore auth")
return nil
}

func getUserToken(header http.Header) (string, error) {
authValue := header.Get(AuthHeader)
if authValue != "" {
parts := strings.Split(authValue, "Bearer ")
if len(parts) != 2 {
return "", errors.New("missing Bearer token in Authorization header")
}
return parts[1], nil
}
return "", errors.New("missing Authorization header")
}

func runTokenReview(ctx context.Context, apiProvider client.APIProvider, token string, preds []tokenReviewPredicate) error {
client, err := apiProvider()
if err != nil {
return err
}

rvw, err := client.CreateTokenReview(ctx, &authv1.TokenReview{
Spec: authv1.TokenReviewSpec{
Token: token,
},
}, &metav1.CreateOptions{})
if err != nil {
return err
}
for _, predFunc := range preds {
if err = predFunc(rvw); err != nil {
return err
}
}
return nil
}

type tokenReviewPredicate func(*authv1.TokenReview) error

func mustBeAuthenticated(rvw *authv1.TokenReview) error {
if !rvw.Status.Authenticated {
return errors.New("user not authenticated")
}
return nil
}

func mustBeClusterAdmin(rvw *authv1.TokenReview) error {
for _, group := range rvw.Status.User.Groups {
if group == "system:cluster-admins" {
return nil
}
}
return errors.New("user not in cluster-admins group")
}

type BearerTokenChecker struct {
Checker
apiProvider client.APIProvider
predicates []tokenReviewPredicate
}

func (c *BearerTokenChecker) CheckAuth(ctx context.Context, header http.Header) error {
hlog.Debug("Checking authenticated user")
token, err := getUserToken(header)
if err != nil {
return err
}
hlog.Debug("Checking auth: token found")
if err = runTokenReview(ctx, c.apiProvider, token, c.predicates); err != nil {
return err
}

hlog.Debug("Checking auth: passed")
return nil
}
Loading

0 comments on commit 04ae8b3

Please sign in to comment.