feature: Basic auth support with fission router (#2292)

This commit is contained in:
Pradeep Lakshmi Narasimha
2022-02-01 19:40:40 +05:30
committed by GitHub
parent 4b307be939
commit 590eefaa52
18 changed files with 466 additions and 6 deletions
+12
View File
@@ -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
+2
View File
@@ -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
}
+10
View File
@@ -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"`
}
)
+7
View File
@@ -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 {
+45
View File
@@ -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
}
+114
View File
@@ -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
}
+4
View File
@@ -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"}
+4
View File
@@ -92,6 +92,10 @@ const (
HtPrefix = "prefix"
HtKeepPrefix = "keepprefix"
TokUsername = "username"
TokPassword = "password"
TokAuthURI = "authuri"
TtName = resourceName
TtCron = "cron"
TtFnName = "function"
+4 -2
View File
@@ -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"
)
+172 -3
View File
@@ -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
}