From 590eefaa52b291e141c6b90b6ba2a8fe9bb91ef2 Mon Sep 17 00:00:00 2001 From: Pradeep Lakshmi Narasimha Date: Tue, 1 Feb 2022 19:40:40 +0530 Subject: [PATCH] feature: Basic auth support with fission router (#2292) --- charts/fission-all/templates/NOTES.txt | 9 + charts/fission-all/templates/_helpers.tpl | 7 + .../templates/router/deployment.yaml | 25 +++ .../fission-all/templates/router/secret.yaml | 14 ++ charts/fission-all/values.yaml | 32 +++- cmd/fission-cli/app/app.go | 2 + go.mod | 1 + go.sum | 3 + pkg/apis/core/v1/types.go | 12 ++ pkg/error/httperror.go | 2 + pkg/featureconfig/types.go | 10 + pkg/fission-cli/cmd/function/test.go | 7 + pkg/fission-cli/cmd/token/command.go | 45 +++++ pkg/fission-cli/cmd/token/create.go | 114 ++++++++++++ pkg/fission-cli/flag/flag.go | 4 + pkg/fission-cli/flag/key/key.go | 4 + pkg/fission-cli/util/constants.go | 6 +- pkg/router/httpTriggers.go | 175 +++++++++++++++++- 18 files changed, 466 insertions(+), 6 deletions(-) create mode 100644 charts/fission-all/templates/router/secret.yaml create mode 100644 pkg/fission-cli/cmd/token/command.go create mode 100644 pkg/fission-cli/cmd/token/create.go diff --git a/charts/fission-all/templates/NOTES.txt b/charts/fission-all/templates/NOTES.txt index 77ffa780..6c8e2239 100644 --- a/charts/fission-all/templates/NOTES.txt +++ b/charts/fission-all/templates/NOTES.txt @@ -20,6 +20,15 @@ Windows: # Register this function with Fission $ fission function create --name hello --env nodejs --code hello.js +{{- if .Values.authentication.enabled }} + + # Create token + $ FISSION_USERNAME=$(kubectl get secrets/router --template={{`{{.data.username}}`}} -n fission | base64 -d) + $ FISSION_PASSWORD=$(kubectl get secrets/router --template={{`{{.data.password}}`}} -n fission | base64 -d) + $ export FISSION_AUTH_TOKEN=$(fission token create --username $FISSION_USERNAME --password $FISSION_PASSWORD) +{{- end }} + # Run this function $ fission function test --name hello Hello, world! + diff --git a/charts/fission-all/templates/_helpers.tpl b/charts/fission-all/templates/_helpers.tpl index ffb3c892..f80ec11d 100644 --- a/charts/fission-all/templates/_helpers.tpl +++ b/charts/fission-all/templates/_helpers.tpl @@ -31,6 +31,13 @@ canary: prometheusSvc: {{ .Values.prometheus.serviceEndpoint | default "" | quote }} {{- end }} {{- printf "\n" -}} +auth: + enabled: {{ .Values.authentication.enabled | default false }} + {{- if .Values.authentication.enabled }} + authUriPath: {{ .Values.authentication.authUriPath | default "/auth/login" | quote}} + jwtExpiryTime: {{ .Values.authentication.jwtExpiryTime | default 120 }} + jwtIssuer: {{ .Values.authentication.jwtIssuer | default "fission" | quote }} + {{- end }} {{- end -}} {{/* diff --git a/charts/fission-all/templates/router/deployment.yaml b/charts/fission-all/templates/router/deployment.yaml index 58b06a9a..5631def4 100644 --- a/charts/fission-all/templates/router/deployment.yaml +++ b/charts/fission-all/templates/router/deployment.yaml @@ -35,6 +35,23 @@ spec: command: ["/fission-bundle"] args: ["--routerPort", "8888", "--executorUrl", "http://executor.{{ .Release.Namespace }}"] env: + {{- if .Values.authentication.enabled }} + - name: AUTH_USERNAME + valueFrom: + secretKeyRef: + name: router + key: username + - name: AUTH_PASSWORD + valueFrom: + secretKeyRef: + name: router + key: password + - name: JWT_SIGNING_KEY + valueFrom: + secretKeyRef: + name: router + key: jwtSigningKey + {{- end }} - name: POD_NAMESPACE valueFrom: fieldRef: @@ -80,6 +97,10 @@ spec: port: 8888 initialDelaySeconds: 35 periodSeconds: 5 + volumeMounts: + - name: config-volume + mountPath: /etc/config/config.yaml + subPath: config.yaml ports: - containerPort: 8080 name: metrics @@ -100,6 +121,10 @@ spec: terminationMessagePolicy: {{ .Values.terminationMessagePolicy }} {{- end }} serviceAccountName: fission-svc + volumes: + - name: config-volume + configMap: + name: feature-config {{- if .Values.router.priorityClassName }} priorityClassName: {{ .Values.router.priorityClassName }} {{- else if .Values.priorityClassName }} diff --git a/charts/fission-all/templates/router/secret.yaml b/charts/fission-all/templates/router/secret.yaml new file mode 100644 index 00000000..fb1ca3ec --- /dev/null +++ b/charts/fission-all/templates/router/secret.yaml @@ -0,0 +1,14 @@ +{{- if .Values.authentication.enabled }} +apiVersion: v1 +kind: Secret +metadata: + name: router + labels: + chart: "{{ .Chart.Name }}-{{ .Chart.Version }}" + annotations: + "helm.sh/hook": pre-install +data: + username: {{ .Values.authentication.authUsername | b64enc | quote }} + password: {{ randAlphaNum 20 | b64enc | quote }} + jwtSigningKey: {{ .Values.authentication.jwtSigningKey | b64enc | quote }} +{{- end }} \ No newline at end of file diff --git a/charts/fission-all/values.yaml b/charts/fission-all/values.yaml index 95bca869..38ccc956 100644 --- a/charts/fission-all/values.yaml +++ b/charts/fission-all/values.yaml @@ -161,7 +161,6 @@ router: ## router resource utilization when under heavy workloads. ## displayAccessLog: false - ## svcAnnotations is the annotations to be added to the service resource created for router. ## # svcAnnotations: @@ -483,6 +482,37 @@ prometheus: canaryDeployment: enabled: false +## Enable authentication for fission function invocation via Fission router +## +authentication: + ## set this flag to true if you need authentication + ## for all function invocations + ## default 'false' + ## + enabled: false + ## authUriPath defines authentication endpoint path + ## via router + ## default '/auth/login' + ## + authUriPath: + ## authUsername is used as a username for authentication + ## default 'admin' + ## + authUsername: admin + ## jwtSigningKey is the signing key used for + ## signing the JWT token + ## + jwtSigningKey: serverless + ## jwtExpiryTime is the JWT expiry time + ## in seconds + ## default '120' + ## + jwtExpiryTime: + ## jwtIssuer is the issuer of JWT + ## default 'fission' + ## + jwtIssuer: fission + ## Use the following flags to enable OpenTracing. ## Note: OpenTracing support will be removed in an upcoming release. ## Please prefer using OpenTelemetry instead. diff --git a/cmd/fission-cli/app/app.go b/cmd/fission-cli/app/app.go index eaa36034..15013705 100644 --- a/cmd/fission-cli/app/app.go +++ b/cmd/fission-cli/app/app.go @@ -19,6 +19,7 @@ import ( "github.com/fission/fission/pkg/fission-cli/cmd/spec" "github.com/fission/fission/pkg/fission-cli/cmd/support" "github.com/fission/fission/pkg/fission-cli/cmd/timetrigger" + "github.com/fission/fission/pkg/fission-cli/cmd/token" "github.com/fission/fission/pkg/fission-cli/cmd/version" "github.com/fission/fission/pkg/fission-cli/console" "github.com/fission/fission/pkg/fission-cli/flag" @@ -77,6 +78,7 @@ func App() *cobra.Command { }) groups := helptemplate.CommandGroups{} + groups = append(groups, helptemplate.CreateCmdGroup("Auth Commands(Note: Authentication should be enabled to use a command in this group.)", token.Commands())) groups = append(groups, helptemplate.CreateCmdGroup("Basic Commands", environment.Commands(), _package.Commands(), function.Commands())) groups = append(groups, helptemplate.CreateCmdGroup("Trigger Commands", httptrigger.Commands(), mqtrigger.Commands(), timetrigger.Commands(), kubewatch.Commands())) groups = append(groups, helptemplate.CreateCmdGroup("Deploy Strategies Commands", canaryconfig.Commands())) diff --git a/go.mod b/go.mod index e0bdcb8a..dc37630d 100644 --- a/go.mod +++ b/go.mod @@ -22,6 +22,7 @@ require ( github.com/go-git/go-git/v5 v5.4.2 github.com/go-ini/ini v1.63.2 // indirect github.com/go-openapi/spec v0.20.4 + github.com/golang-jwt/jwt/v4 v4.2.0 github.com/gorilla/mux v1.8.0 github.com/gotestyourself/gotestyourself v2.2.0+incompatible // indirect github.com/graymeta/stow v0.2.7 diff --git a/go.sum b/go.sum index c7a91951..85647d14 100644 --- a/go.sum +++ b/go.sum @@ -492,7 +492,10 @@ github.com/gogo/protobuf v1.2.2-0.20190730201129-28a6bbf47e48/go.mod h1:SlYgWuQ5 github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang-jwt/jwt v3.2.1+incompatible h1:73Z+4BJcrTC+KczS6WvTPvRGOp1WmfEP4Q1lOd9Z/+c= github.com/golang-jwt/jwt v3.2.1+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= +github.com/golang-jwt/jwt/v4 v4.2.0 h1:besgBTC8w8HjP6NzQdxwKH9Z5oQMZ24ThTrHp3cZ8eU= +github.com/golang-jwt/jwt/v4 v4.2.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg= github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0= github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g= github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k= diff --git a/pkg/apis/core/v1/types.go b/pkg/apis/core/v1/types.go index bb3416cd..99fdbe7e 100644 --- a/pkg/apis/core/v1/types.go +++ b/pkg/apis/core/v1/types.go @@ -854,6 +854,18 @@ type ( GetObjectKind() schema.ObjectKind GetObjectMeta() metav1.Object } + + // AuthLogin defines the body for router login + AuthLogin struct { + Username string `json:"username"` + Password string `json:"password"` + } + + // RouterAuthToken defines the authorization token for accessing router + RouterAuthToken struct { + AccessToken string `json:"accesstoken"` + TokenType string `json:"tokentype"` + } ) //IsEmpty checks if the archive byte and litreal are of length 0 diff --git a/pkg/error/httperror.go b/pkg/error/httperror.go index 60a3db5a..49cde0be 100644 --- a/pkg/error/httperror.go +++ b/pkg/error/httperror.go @@ -60,6 +60,8 @@ func MakeErrorFromHTTP(resp *http.Response) error { errCode = ErrorRequestTimeout case http.StatusTooManyRequests: errCode = ErrorTooManyRequests + case http.StatusUnauthorized: + errCode = ErrorNotAuthorized default: errCode = ErrorInternal } diff --git a/pkg/featureconfig/types.go b/pkg/featureconfig/types.go index a39a5bef..87ababda 100644 --- a/pkg/featureconfig/types.go +++ b/pkg/featureconfig/types.go @@ -16,6 +16,8 @@ limitations under the License. package featureconfig +import "time" + const ( FeatureConfigFile = "/etc/config/config.yaml" CanaryFeature = "canary" @@ -31,6 +33,7 @@ type ( FeatureConfig struct { // In the future more such feature configs can be added here for each optional feature CanaryConfig CanaryFeatureConfig `json:"canary"` + AuthConfig AuthFeatureConfig `json:"auth"` } // specific feature config @@ -38,4 +41,11 @@ type ( IsEnabled bool `json:"enabled"` PrometheusSvc string `json:"prometheusSvc"` } + + AuthFeatureConfig struct { + IsEnabled bool `json:"enabled"` + AuthUriPath string `json:"authUriPath"` + JWTExpiryTime time.Duration `json:"jwtExpiryTime"` + JWTIssuer string `json:"jwtIssuer"` + } ) diff --git a/pkg/fission-cli/cmd/function/test.go b/pkg/fission-cli/cmd/function/test.go index 1dbb6d1f..f3e4288a 100644 --- a/pkg/fission-cli/cmd/function/test.go +++ b/pkg/fission-cli/cmd/function/test.go @@ -18,6 +18,7 @@ package function import ( "context" + "fmt" "io" "net/http" "net/url" @@ -178,6 +179,11 @@ func doHTTPRequest(ctx context.Context, url string, headers []string, method, bo return nil, errors.Wrap(err, "error creating HTTP request") } + accesstoken, ok := os.LookupEnv(util.FISSION_AUTH_TOKEN) + if ok && len(accesstoken) != 0 { + req.Header.Set("Authorization", fmt.Sprintf("Bearer %v", accesstoken)) + } + for _, header := range headers { headerKeyValue := strings.SplitN(header, ":", 2) if len(headerKeyValue) != 2 { @@ -185,6 +191,7 @@ func doHTTPRequest(ctx context.Context, url string, headers []string, method, bo } req.Header.Set(headerKeyValue[0], headerKeyValue[1]) } + hc := &http.Client{Transport: otelhttp.NewTransport(http.DefaultTransport)} resp, err := hc.Do(req.WithContext(ctx)) if err != nil { diff --git a/pkg/fission-cli/cmd/token/command.go b/pkg/fission-cli/cmd/token/command.go new file mode 100644 index 00000000..f36c5e28 --- /dev/null +++ b/pkg/fission-cli/cmd/token/command.go @@ -0,0 +1,45 @@ +/* +Copyright 2022 The Fission Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package token + +import ( + "github.com/spf13/cobra" + + wrapper "github.com/fission/fission/pkg/fission-cli/cliwrapper/driver/cobra" + "github.com/fission/fission/pkg/fission-cli/flag" +) + +func Commands() *cobra.Command { + createCmd := &cobra.Command{ + Use: "create", + Short: "Create a JWT token for function invocation", + RunE: wrapper.Wrapper(Create), + } + wrapper.SetFlags(createCmd, flag.FlagSet{ + Required: []flag.Flag{flag.TokUsername, flag.TokPassword}, + Optional: []flag.Flag{flag.TokAuthURI}, + }) + + command := &cobra.Command{ + Use: "token", + Short: "Create a JWT token for function invocation", + } + + command.AddCommand(createCmd) + + return command +} diff --git a/pkg/fission-cli/cmd/token/create.go b/pkg/fission-cli/cmd/token/create.go new file mode 100644 index 00000000..64d6d2b9 --- /dev/null +++ b/pkg/fission-cli/cmd/token/create.go @@ -0,0 +1,114 @@ +/* +Copyright 2022 The Fission Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package token + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "net/http" + "net/url" + "os" + + "github.com/pkg/errors" + + fv1 "github.com/fission/fission/pkg/apis/core/v1" + "github.com/fission/fission/pkg/fission-cli/cliwrapper/cli" + "github.com/fission/fission/pkg/fission-cli/cmd" + flagkey "github.com/fission/fission/pkg/fission-cli/flag/key" + "github.com/fission/fission/pkg/fission-cli/util" +) + +type CreateSubCommand struct { + cmd.CommandActioner +} + +func Create(input cli.Input) error { + return (&CreateSubCommand{}).do(input) +} + +func (opts *CreateSubCommand) do(input cli.Input) error { + return opts.run(input) +} + +func (opts *CreateSubCommand) run(input cli.Input) error { + + lb := &fv1.AuthLogin{} + + username := input.String(flagkey.TokUsername) + if len(username) != 0 { + lb.Username = username + } + + password := input.String(flagkey.TokPassword) + if len(password) != 0 { + lb.Password = password + } + + values := map[string]string{"username": username, "password": password} + + jsonValue, _ := json.Marshal(values) + + kubeContext := input.String(flagkey.KubeContext) + // Portforward to the fission router + localRouterPort, err := util.SetupPortForward(util.GetFissionNamespace(), "application=fission-router", kubeContext) + if err != nil { + return err + } + + authURI, _ := os.LookupEnv("FISSION_AUTH_URI") + + if input.IsSet(flagkey.TokAuthURI) { + authURI = input.String(flagkey.TokAuthURI) + } + + if len(authURI) == 0 { + authURI = util.FISSION_AUTH_URI + } + + relativeURL, _ := url.Parse(authURI) + serverURL, _ := url.Parse("http://127.0.0.1:" + localRouterPort) + authAuthenticatorUrl := serverURL.ResolveReference(relativeURL) + + resp, err := http.Post(authAuthenticatorUrl.String(), "application/json", bytes.NewBuffer(jsonValue)) + if err != nil { + return err + } + + defer resp.Body.Close() + + body, err := io.ReadAll(resp.Body) + if err != nil { + return errors.Wrap(err, "error creating token") + } + + if resp.StatusCode == http.StatusCreated { + var rat fv1.RouterAuthToken + err = json.Unmarshal(body, &rat) + if err != nil { + return err + } + fmt.Println(rat.AccessToken) + } else if resp.StatusCode == http.StatusNotFound { + fmt.Printf("%s. Please check if authentication is enabled and correct auth URI is mentioned via --authuri or FISSION_AUTH_URI.\n", resp.Status) + } else { + fmt.Println(resp.Status) + } + + return nil +} diff --git a/pkg/fission-cli/flag/flag.go b/pkg/fission-cli/flag/flag.go index 9c2131a8..ecccb3d0 100644 --- a/pkg/fission-cli/flag/flag.go +++ b/pkg/fission-cli/flag/flag.go @@ -142,6 +142,10 @@ var ( HtPrefix = Flag{Type: String, Name: flagkey.HtPrefix, Usage: "Prefix with which functions are exposed. NOTE: Prefix takes precedence over URL/RelativeURL [DEPRECATED for 'fn create', use 'route create' instead]"} HtKeepPrefix = Flag{Type: Bool, Name: flagkey.HtKeepPrefix, Usage: "Keep the prefix in the URL while forwarding request to the function"} + TokUsername = Flag{Type: String, Name: flagkey.TokUsername, Usage: "Username to generate token for function invocation"} + TokPassword = Flag{Type: String, Name: flagkey.TokPassword, Usage: "Password to generate token for function invocation"} + TokAuthURI = Flag{Type: String, Name: flagkey.TokAuthURI, Usage: "Relative URI path to generate token"} + TtName = Flag{Type: String, Name: flagkey.TtName, Usage: "Time Trigger name"} TtCron = Flag{Type: String, Name: flagkey.TtCron, Usage: "Time trigger cron spec with each asterisk representing respectively second, minute, hour, the day of the month, month and day of the week. Also supports readable formats like '@every 5m', '@hourly'"} TtFnName = Flag{Type: String, Name: flagkey.TtFnName, Usage: "Function name"} diff --git a/pkg/fission-cli/flag/key/key.go b/pkg/fission-cli/flag/key/key.go index c736364b..5365a482 100644 --- a/pkg/fission-cli/flag/key/key.go +++ b/pkg/fission-cli/flag/key/key.go @@ -92,6 +92,10 @@ const ( HtPrefix = "prefix" HtKeepPrefix = "keepprefix" + TokUsername = "username" + TokPassword = "password" + TokAuthURI = "authuri" + TtName = resourceName TtCron = "cron" TtFnName = "function" diff --git a/pkg/fission-cli/util/constants.go b/pkg/fission-cli/util/constants.go index 2b386e5b..bca1af5d 100644 --- a/pkg/fission-cli/util/constants.go +++ b/pkg/fission-cli/util/constants.go @@ -18,6 +18,8 @@ package util // fission-cli options const ( - SPEC_IGNORE_FILE = ".specignore" - COMMIT_LABEL = "commit" + SPEC_IGNORE_FILE = ".specignore" + COMMIT_LABEL = "commit" + FISSION_AUTH_URI = "/auth/login" + FISSION_AUTH_TOKEN = "FISSION_AUTH_TOKEN" ) diff --git a/pkg/router/httpTriggers.go b/pkg/router/httpTriggers.go index 19776817..9bf62d68 100644 --- a/pkg/router/httpTriggers.go +++ b/pkg/router/httpTriggers.go @@ -18,10 +18,16 @@ package router import ( "context" + "encoding/json" + "errors" + "fmt" + "io" "net/http" + "os" "strings" "time" + "github.com/golang-jwt/jwt/v4" "github.com/gorilla/mux" "go.uber.org/zap" "k8s.io/apimachinery/pkg/types" @@ -31,6 +37,7 @@ import ( fv1 "github.com/fission/fission/pkg/apis/core/v1" "github.com/fission/fission/pkg/crd" executorClient "github.com/fission/fission/pkg/executor/client" + config "github.com/fission/fission/pkg/featureconfig" genInformer "github.com/fission/fission/pkg/generated/informers/externalversions" "github.com/fission/fission/pkg/throttler" "github.com/fission/fission/pkg/utils" @@ -38,6 +45,8 @@ import ( "github.com/fission/fission/pkg/utils/tracing" ) +var featureConfig *config.FeatureConfig + // HTTPTriggerSet represents an HTTP trigger set type HTTPTriggerSet struct { *functionServiceMap @@ -59,6 +68,20 @@ type HTTPTriggerSet struct { unTapServiceTimeout time.Duration } +func init() { + _ = loadFeatureConfigmap() +} + +func loadFeatureConfigmap() error { + var err error + featureConfig, err = config.GetFeatureConfig() + if err != nil { + fmt.Println(err) + return errors.New("error while loading feature configmap") + } + return nil +} + func makeHTTPTriggerSet(logger *zap.Logger, fmap *functionServiceMap, fissionClient *crd.FissionClient, kubeClient *kubernetes.Clientset, executor *executorClient.Client, params *tsRoundTripperParams, isDebugEnv bool, unTapServiceTimeout time.Duration, actionThrottler *throttler.Throttler) *HTTPTriggerSet { @@ -106,10 +129,92 @@ func defaultHomeHandler(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) } +func createErrorResponse(errMsg string, statusCode int) []byte { + resp, _ := json.Marshal(map[string]interface{}{"statusCode": statusCode, "message": errMsg}) + return resp +} + func routerHealthHandler(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) } +func authLoginHandler(w http.ResponseWriter, r *http.Request) { + + body, err := io.ReadAll(r.Body) + if err != nil { + w.WriteHeader(http.StatusBadRequest) + _, _ = w.Write(createErrorResponse("Error while reading request body", http.StatusBadRequest)) + return + } + + var t fv1.AuthLogin + + err = json.Unmarshal(body, &t) + if err != nil { + w.WriteHeader(http.StatusBadRequest) + _, _ = w.Write(createErrorResponse("Error while reading request body", http.StatusBadRequest)) + return + } + + username, ok := os.LookupEnv("AUTH_USERNAME") + if !ok || len(username) == 0 { + w.WriteHeader(http.StatusBadRequest) + _, _ = w.Write(createErrorResponse("Username not found or invalid", http.StatusBadRequest)) + return + } + + password, ok := os.LookupEnv("AUTH_PASSWORD") + if !ok || len(password) == 0 { + w.WriteHeader(http.StatusBadRequest) + _, _ = w.Write(createErrorResponse("Password not found or invalid", http.StatusBadRequest)) + return + } + + signingKey, ok := os.LookupEnv("JWT_SIGNING_KEY") + if !ok || len(signingKey) == 0 { + w.WriteHeader(http.StatusInternalServerError) + _, _ = w.Write(createErrorResponse("Internal server error occurred", http.StatusInternalServerError)) + return + } + + rat := &fv1.RouterAuthToken{} + + if t.Username == username && t.Password == password { + + claims := &jwt.RegisteredClaims{ + ExpiresAt: jwt.NewNumericDate(jwt.TimeFunc().Add(featureConfig.AuthConfig.JWTExpiryTime * time.Second)), + Issuer: featureConfig.AuthConfig.JWTIssuer, + NotBefore: jwt.NewNumericDate(jwt.TimeFunc()), + } + + token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) + ss, err := token.SignedString([]byte(signingKey)) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + _, _ = w.Write(createErrorResponse("Internal server error occurred", http.StatusInternalServerError)) + return + } + rat.AccessToken = ss + rat.TokenType = "Bearer" + + } else { + w.WriteHeader(http.StatusUnauthorized) + _, _ = w.Write(createErrorResponse("Unauthorized: invalid username and/or password", http.StatusUnauthorized)) + return + } + + resp, err := json.Marshal(rat) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + _, _ = w.Write(createErrorResponse("Internal server error occurred", http.StatusInternalServerError)) + return + } + + w.WriteHeader(http.StatusCreated) + _, _ = w.Write(resp) + +} + func (ts *HTTPTriggerSet) getRouter(fnTimeoutMap map[types.UID]int) *mux.Router { muxRouter := mux.NewRouter() @@ -199,7 +304,12 @@ func (ts *HTTPTriggerSet) getRouter(fnTimeoutMap map[types.UID]int) *mux.Router } ts.logger.Debug("add prefix route for function", zap.String("route", prefix), zap.Any("function", fh.function), zap.Strings("methods", methods)) } else { - ht1 := muxRouter.Handle(prefix, handler) + var ht1 *mux.Route + if featureConfig.AuthConfig.IsEnabled { + ht1 = muxRouter.Handle(prefix, authMiddleware(handler)) + } else { + ht1 = muxRouter.Handle(prefix, handler) + } ht1.Methods(methods...) if trigger.Spec.Host != "" { ht1.Host(trigger.Spec.Host) @@ -212,7 +322,12 @@ func (ts *HTTPTriggerSet) getRouter(fnTimeoutMap map[types.UID]int) *mux.Router ts.logger.Debug("add prefix and handler route for function", zap.String("route", prefix), zap.Any("function", fh.function), zap.Strings("methods", methods)) } } else { - ht := muxRouter.Handle(trigger.Spec.RelativeURL, handler) + var ht *mux.Route + if featureConfig.AuthConfig.IsEnabled { + ht = muxRouter.Handle(trigger.Spec.RelativeURL, authMiddleware(handler)) + } else { + ht = muxRouter.Handle(trigger.Spec.RelativeURL, handler) + } ht.Methods(methods...) if trigger.Spec.Host != "" { ht.Host(trigger.Spec.Host) @@ -259,17 +374,71 @@ func (ts *HTTPTriggerSet) getRouter(fnTimeoutMap map[types.UID]int) *mux.Router } else { handler = otel.GetHandlerWithOTEL(http.HandlerFunc(fh.handler), internalRoute) } - muxRouter.Handle(internalRoute, handler) + + if featureConfig.AuthConfig.IsEnabled { + muxRouter.Handle(internalRoute, authMiddleware(handler)) + } else { + muxRouter.Handle(internalRoute, handler) + } muxRouter.PathPrefix(internalPrefixRoute).Handler(handler) ts.logger.Debug("add internal handler and prefix route for function", zap.String("router", internalRoute), zap.Any("function", fn)) } + if featureConfig.AuthConfig.IsEnabled { + + path := featureConfig.AuthConfig.AuthUriPath + if len(path) == 0 { + path = "/auth/login" + } + + // Auth endpoint for the router. + muxRouter.HandleFunc(path, authLoginHandler).Methods("POST") + } + // Healthz endpoint for the router. muxRouter.HandleFunc("/router-healthz", routerHealthHandler).Methods("GET") return muxRouter } +func authMiddleware(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + + authHeader := strings.Split(r.Header.Get("Authorization"), "Bearer ") + if len(authHeader) != 2 || len(authHeader[1]) == 0 { + // malformed token + w.WriteHeader(http.StatusUnauthorized) + _, _ = w.Write(createErrorResponse("Unauthorized: malformed Token", http.StatusUnauthorized)) + } else { + jwtToken := authHeader[1] + token, err := jwt.Parse(jwtToken, func(token *jwt.Token) (interface{}, error) { + return []byte(os.Getenv("JWT_SIGNING_KEY")), nil + }) + + if token != nil && token.Valid { + // valid token + next.ServeHTTP(w, r) + } else if ve, ok := err.(*jwt.ValidationError); ok { + w.WriteHeader(http.StatusUnauthorized) + if ve.Errors&jwt.ValidationErrorMalformed != 0 { + // malformed token + _, _ = w.Write(createErrorResponse("Unauthorized: malformed Token", http.StatusUnauthorized)) + } else if ve.Errors&(jwt.ValidationErrorExpired|jwt.ValidationErrorNotValidYet) != 0 { + // token is either expired or not active yet + _, _ = w.Write(createErrorResponse("Unauthorized: token is either expired or not active yet", http.StatusUnauthorized)) + } else { + _, _ = w.Write(createErrorResponse(fmt.Sprintf("Unauthorized: %v", err.Error()), http.StatusUnauthorized)) + } + } else { + w.WriteHeader(http.StatusUnauthorized) + _, _ = w.Write(createErrorResponse("Unauthorized", http.StatusUnauthorized)) + } + + } + + }) +} + func (ts *HTTPTriggerSet) updateTriggerStatusFailed(ht *fv1.HTTPTrigger, err error) { // TODO }