diff --git a/config/config.go b/config/config.go index f1572d79..3c6f9e3d 100644 --- a/config/config.go +++ b/config/config.go @@ -57,6 +57,13 @@ type WatchdogConfig struct { // using the OpenFaaS gateway as the issuer. JWTAuthentication bool + // JWTAuthDebug enables debug logging for the JWT authentication middleware. + JWTAuthDebug bool + + // JWTAuthLocal indicates wether the JWT authentication middleware should use a port-forwarded or + // local gateway running at `http://127.0.0.1:8000` instead of attempting to reach it via an in-cluster service + JWTAuthLocal bool + // LogCallId includes a prefix of the X-Call-Id in any log statements in // HTTP mode. LogCallId bool @@ -178,6 +185,8 @@ func New(env []string) (WatchdogConfig, error) { } c.JWTAuthentication = getBool(envMap, "jwt_auth") + c.JWTAuthDebug = getBool(envMap, "jwt_auth_debug") + c.JWTAuthLocal = getBool(envMap, "jwt_auth_local") return c, nil } diff --git a/executor/jwt_authenticator.go b/executor/jwt_authenticator.go deleted file mode 100644 index 578f5834..00000000 --- a/executor/jwt_authenticator.go +++ /dev/null @@ -1,300 +0,0 @@ -package executor - -import ( - "crypto" - "encoding/json" - "fmt" - "io" - "log" - "net/http" - "os" - "regexp" - "strings" - "time" - - "github.com/rakutentech/jwk-go/jwk" - - "github.com/golang-jwt/jwt/v5" -) - -const functionRealm = "IAM function invoke" - -func NewJWTAuthMiddleware(next http.Handler) (http.Handler, error) { - var authority = "http://gateway.openfaas:8080/.well-known/openid-configuration" - if v, ok := os.LookupEnv("jwt_auth_local"); ok && (v == "true" || v == "1") { - authority = "http://127.0.0.1:8000/.well-known/openid-configuration" - } - - jwtAuthDebug := false - if val, ok := os.LookupEnv("jwt_auth_debug"); ok && val == "true" || val == "1" { - jwtAuthDebug = true - } - - config, err := getConfig(authority) - if err != nil { - return nil, err - } - - if jwtAuthDebug { - log.Printf("[JWT Auth] Issuer: %s\tJWKS URI: %s", config.Issuer, config.JWKSURI) - } - - keyset, err := getKeyset(config.JWKSURI) - if err != nil { - return nil, err - } - - if jwtAuthDebug { - for _, key := range keyset.Keys { - log.Printf("[JWT Auth] Key: %s", key.KeyID) - } - } - - issuer := config.Issuer - - namespace, err := getFnNamespace() - if err != nil { - return nil, fmt.Errorf("failed to get function namespace: %s", err) - } - name, err := getFnName() - if err != nil { - return nil, fmt.Errorf("failed to get function name: %s", err) - } - - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - st := time.Now() - for _, key := range keyset.Keys { - log.Printf("%s: %v", issuer, key.KeyID) - } - - var bearer string - if v := r.Header.Get("Authorization"); v != "" { - bearer = strings.TrimPrefix(v, "Bearer ") - } - - if bearer == "" { - httpUnauthorized(w, "Bearer must be present in Authorization header") - log.Printf("%s %s - %d ACCESS DENIED - (%s)", r.Method, r.URL.Path, http.StatusUnauthorized, time.Since(st).Round(time.Millisecond)) - return - } - - parseOptions := []jwt.ParserOption{ - jwt.WithIssuer(issuer), - // The OpenFaaS gateway is the expected audience but we can use the issuer url - // since the gateway is also the issuer of function tokens and thus has the same url. - jwt.WithAudience(issuer), - jwt.WithLeeway(time.Second * 1), - } - - functionClaims := FunctionClaims{} - token, err := jwt.ParseWithClaims(bearer, &functionClaims, func(token *jwt.Token) (interface{}, error) { - if jwtAuthDebug { - log.Printf("[JWT Auth] Token: audience: %v\tissuer: %v", functionClaims.Audience, functionClaims.Issuer) - } - - kid, ok := token.Header["kid"].(string) - if !ok { - return nil, fmt.Errorf("invalid kid: %v", token.Header["kid"]) - } - var key *jwk.KeySpec - for _, k := range keyset.Keys { - if k.KeyID == kid { - key = &k - break - } - } - - if key == nil { - return nil, fmt.Errorf("invalid kid: %s", kid) - } - return key.Key.(crypto.PublicKey), nil - }, parseOptions...) - if err != nil { - httpUnauthorized(w, fmt.Sprintf("failed to parse JWT token: %s", err)) - log.Printf("%s %s - %d ACCESS DENIED - (%s)", r.Method, r.URL.Path, http.StatusUnauthorized, time.Since(st).Round(time.Millisecond)) - return - } - - if !token.Valid { - httpUnauthorized(w, fmt.Sprintf("invalid JWT token: %s", bearer)) - - log.Printf("%s %s - %d ACCESS DENIED - (%s)", r.Method, r.URL.Path, http.StatusUnauthorized, time.Since(st).Round(time.Millisecond)) - return - } - - if !isAuthorized(functionClaims.Authentication, namespace, name) { - http.Error(w, "insufficient permissions", http.StatusForbidden) - - log.Printf("%s %s - %d ACCESS DENIED - (%s)", r.Method, r.URL.Path, http.StatusForbidden, time.Since(st).Round(time.Millisecond)) - return - } - - next.ServeHTTP(w, r) - }), nil -} - -// httpUnauthorized replies to the request with the specified error message and 401 HTTP code. -// It sets the WWW-Authenticate header. -// It does not otherwise end the request; the caller should ensure no further writes are done to w. -// The error message should be plain text. -func httpUnauthorized(w http.ResponseWriter, err string) { - w.Header().Set("WWW-Authenticate", fmt.Sprintf("Bearer realm=%s", functionRealm)) - http.Error(w, err, http.StatusUnauthorized) -} - -func getKeyset(uri string) (jwk.KeySpecSet, error) { - var set jwk.KeySpecSet - req, err := http.NewRequest(http.MethodGet, uri, nil) - if err != nil { - return set, err - } - - req.Header.Add("User-Agent", "openfaas-watchdog") - - res, err := http.DefaultClient.Do(req) - if err != nil { - return set, err - } - - var body []byte - - if res.Body != nil { - defer res.Body.Close() - body, _ = io.ReadAll(res.Body) - } - - if res.StatusCode != http.StatusOK { - return set, fmt.Errorf("failed to get keyset from %s, status code: %d, body: %s", uri, res.StatusCode, string(body)) - } - - if err := json.Unmarshal(body, &set); err != nil { - return set, err - } - - return set, nil -} - -func getConfig(jwksURL string) (OpenIDConfiguration, error) { - var config OpenIDConfiguration - - req, err := http.NewRequest(http.MethodGet, jwksURL, nil) - if err != nil { - return config, err - } - - res, err := http.DefaultClient.Do(req) - if err != nil { - return config, err - } - - var body []byte - if res.Body != nil { - defer res.Body.Close() - body, _ = io.ReadAll(res.Body) - } - - if res.StatusCode != http.StatusOK { - return config, fmt.Errorf("failed to get config from %s, status code: %d, body: %s", jwksURL, res.StatusCode, string(body)) - } - - if err := json.Unmarshal(body, &config); err != nil { - return config, err - } - - return config, nil -} - -type OpenIDConfiguration struct { - Issuer string `json:"issuer"` - JWKSURI string `json:"jwks_uri"` -} - -type FunctionClaims struct { - jwt.RegisteredClaims - - Authentication AuthPermissions `json:"function"` -} - -type AuthPermissions struct { - Permissions []string `json:"permissions"` - Audience []string `json:"audience,omitempty"` -} - -func isAuthorized(auth AuthPermissions, namespace, fn string) bool { - functionRef := fmt.Sprintf("%s:%s", namespace, fn) - - return matchResource(auth.Audience, functionRef, false) && - matchResource(auth.Permissions, functionRef, true) -} - -// matchResources checks if ref matches one of the resources. -// The function will return true if a match is found. -// If required is false, this function will return true if a match is found or the resource list is empty. -func matchResource(resources []string, ref string, req bool) bool { - if !req { - if len(resources) == 0 { - return true - } - } - - for _, res := range resources { - if res == "*" { - return true - } - - if matchString(res, ref) { - return true - } - } - - return false -} - -func matchString(pattern string, value string) bool { - if len(pattern) > 0 { - result, _ := regexp.MatchString(wildCardToRegexp(pattern), value) - return result - } - - return pattern == value -} - -// wildCardToRegexp converts a wildcard pattern to a regular expression pattern. -func wildCardToRegexp(pattern string) string { - var result strings.Builder - for i, literal := range strings.Split(pattern, "*") { - - // Replace * with .* - if i > 0 { - result.WriteString(".*") - } - - // Quote any regular expression meta characters in the - // literal text. - result.WriteString(regexp.QuoteMeta(literal)) - } - return result.String() -} - -func getFnName() (string, error) { - name, ok := os.LookupEnv("OPENFAAS_NAME") - if !ok || len(name) == 0 { - return "", fmt.Errorf("env variable 'OPENFAAS_NAME' not set") - } - - return name, nil -} - -// getFnNamespace gets the namespace name from the env variable OPENFAAS_NAMESPACE -// or reads it from the service account if the env variable is not present -func getFnNamespace() (string, error) { - if namespace, ok := os.LookupEnv("OPENFAAS_NAMESPACE"); ok { - return namespace, nil - } - - nsVal, err := os.ReadFile("/var/run/secrets/kubernetes.io/serviceaccount/namespace") - if err != nil { - return "", err - } - return string(nsVal), nil -} diff --git a/executor/jwt_authenticator_test.go b/executor/jwt_authenticator_test.go deleted file mode 100644 index 8c1993e0..00000000 --- a/executor/jwt_authenticator_test.go +++ /dev/null @@ -1,186 +0,0 @@ -package executor - -import ( - "testing" -) - -func Test_isAuthorized(t *testing.T) { - tests := []struct { - name string - want bool - permissions AuthPermissions - namespace string - function string - }{ - { - name: "deny empty permission list", - want: false, - permissions: AuthPermissions{ - Permissions: []string{}, - }, - namespace: "staging", - function: "env", - }, - { - name: "allow empty audience list", - want: true, - permissions: AuthPermissions{ - Permissions: []string{"staging:env"}, - }, - namespace: "staging", - function: "env", - }, - { - name: "allow cluster wildcard", - want: true, - permissions: AuthPermissions{ - Permissions: []string{"*"}, - }, - namespace: "staging", - function: "figlet", - }, - { - name: "allow function wildcard", - want: true, - permissions: AuthPermissions{ - Permissions: []string{"dev:*"}, - }, - namespace: "dev", - function: "figlet", - }, - { - name: "allow namespace wildcard", - want: true, - permissions: AuthPermissions{ - Permissions: []string{"*:env"}, - }, - namespace: "openfaas-fn", - function: "env", - }, - { - name: "allow function", - want: true, - permissions: AuthPermissions{ - Permissions: []string{"openfaas-fn:env"}, - }, - namespace: "openfaas-fn", - function: "env", - }, - { - name: "deny function", - want: false, - permissions: AuthPermissions{ - Permissions: []string{"openfaas-fn:env"}, - }, - namespace: "openfaas-fn", - function: "figlet", - }, - { - name: "deny namespace", - want: false, - permissions: AuthPermissions{ - Permissions: []string{"openfaas-fn:*"}, - }, - namespace: "staging", - function: "env", - }, - { - name: "deny namespace wildcard", - want: false, - permissions: AuthPermissions{ - Permissions: []string{"*:figlet"}, - }, - namespace: "staging", - function: "env", - }, - { - name: "multiple permissions allow function", - want: true, - permissions: AuthPermissions{ - Permissions: []string{"openfaas-fn:*", "staging:env"}, - }, - namespace: "staging", - function: "env", - }, - { - name: "multiple permissions deny function", - want: false, - permissions: AuthPermissions{ - Permissions: []string{"openfaas-fn:figlet", "staging-*:env"}, - }, - namespace: "staging", - function: "env", - }, - { - name: "allow audience", - want: true, - permissions: AuthPermissions{ - Permissions: []string{"openfaas-fn:*"}, - Audience: []string{"openfaas-fn:env"}, - }, - namespace: "openfaas-fn", - function: "env", - }, - { - name: "deny audience", - want: false, - permissions: AuthPermissions{ - Permissions: []string{"openfaas-fn:*"}, - Audience: []string{"openfaas-fn:env"}, - }, - namespace: "openfaas-fn", - function: "figlet", - }, - { - name: "allow audience function wildcard", - want: true, - permissions: AuthPermissions{ - Permissions: []string{"openfaas-fn:figlet"}, - Audience: []string{"openfaas-fn:*"}, - }, - namespace: "openfaas-fn", - function: "figlet", - }, - { - name: "deny audience function wildcard", - want: false, - permissions: AuthPermissions{ - Permissions: []string{"openfaas-fn:figlet", "dev:env"}, - Audience: []string{"openfaas-fn:*"}, - }, - namespace: "dev", - function: "env", - }, - { - name: "deny audience namespace wildcard", - want: false, - permissions: AuthPermissions{ - Permissions: []string{"openfaas-fn:*", "dev:*"}, - Audience: []string{"*:env"}, - }, - namespace: "dev", - function: "figlet", - }, - { - name: "allow audience namespace wildcard", - want: true, - permissions: AuthPermissions{ - Permissions: []string{"openfaas-fn:*", "dev:*"}, - Audience: []string{"*:env"}, - }, - namespace: "openfaas-fn", - function: "env", - }, - } - - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - want := test.want - got := isAuthorized(test.permissions, test.namespace, test.function) - - if want != got { - t.Errorf("want: %t, got: %t", want, got) - } - }) - } -} diff --git a/go.mod b/go.mod index 9bda8ec2..c1a39ea7 100644 --- a/go.mod +++ b/go.mod @@ -2,13 +2,13 @@ module github.com/openfaas/of-watchdog go 1.21 +replace github.com/openfaas/faas-middleware => /home/welteki/code/openfaas/oss/faas-middleware + require ( github.com/docker/go-units v0.5.0 - github.com/golang-jwt/jwt/v5 v5.2.1 github.com/openfaas/faas-middleware v1.2.3 github.com/openfaas/faas-provider v0.25.3 github.com/prometheus/client_golang v1.19.0 - github.com/rakutentech/jwk-go v1.1.3 ) require ( @@ -22,6 +22,8 @@ require ( github.com/beorn7/perks v1.0.1 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/davecgh/go-spew v1.1.1 // indirect + github.com/golang-jwt/jwt/v5 v5.2.1 // indirect + github.com/rakutentech/jwk-go v1.1.3 // indirect golang.org/x/crypto v0.22.0 // indirect google.golang.org/protobuf v1.33.0 // indirect ) diff --git a/go.sum b/go.sum index c73433ee..2d77824f 100644 --- a/go.sum +++ b/go.sum @@ -20,8 +20,6 @@ github.com/onsi/ginkgo v1.12.0/go.mod h1:oUhWkIvk5aDxtKvDDuw8gItl8pKl42LzjC9KZE0 github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= github.com/onsi/gomega v1.9.0 h1:R1uwffexN6Pr340GtYRIdZmAiN4J+iw6WG4wog1DUXg= github.com/onsi/gomega v1.9.0/go.mod h1:Ho0h+IUsWyvy1OpqCwxlQ/21gkhVunqlU8fDGcoTdcA= -github.com/openfaas/faas-middleware v1.2.3 h1:nRib38/i5eNdUTTKA7ILgO/Xns5zVorCO6lIBjr2xA0= -github.com/openfaas/faas-middleware v1.2.3/go.mod h1:pMyWe0SP0zuzIj2on1pmRkZAjGIS+uRk2mp3N6LSlDI= github.com/openfaas/faas-provider v0.25.3 h1:cy5GKP1R/xZkPjg+9We7yqpfz298GrKw4ZRYJVprt7Q= github.com/openfaas/faas-provider v0.25.3/go.mod h1:NsETIfEndZn4mn/w/XnBTcDTwKqULCziphLp7KgeRcA= github.com/prometheus/client_golang v1.19.0 h1:ygXvpU1AoN1MhdzckN+PyD9QJOSD4x7kmXYlnfbA6JU= diff --git a/main.go b/main.go index ab483d16..21bfc5d7 100644 --- a/main.go +++ b/main.go @@ -22,6 +22,7 @@ import ( units "github.com/docker/go-units" + "github.com/openfaas/faas-middleware/auth" limiter "github.com/openfaas/faas-middleware/concurrency-limiter" "github.com/openfaas/of-watchdog/config" "github.com/openfaas/of-watchdog/executor" @@ -75,12 +76,12 @@ func main() { requestHandler := baseFunctionHandler if watchdogConfig.JWTAuthentication { - handler, err := executor.NewJWTAuthMiddleware(baseFunctionHandler) + handler, err := makeJWTAuthHandler(watchdogConfig, baseFunctionHandler) if err != nil { log.Fatalf("Error creating JWTAuthMiddleware: %s", err.Error()) } - requestHandler = handler + requestHandler = handler } var limit limiter.Limiter @@ -443,6 +444,26 @@ func makeHealthHandler() func(http.ResponseWriter, *http.Request) { } } +func makeJWTAuthHandler(c config.WatchdogConfig, next http.Handler) (http.Handler, error) { + namespace, err := getFnNamespace() + if err != nil { + return nil, fmt.Errorf("failed to get function namespace: %w", err) + } + name, err := getFnName() + if err != nil { + return nil, fmt.Errorf("failed to get function name: %w", err) + } + + authOpts := auth.JWTAuthOptions{ + Name: name, + Namespace: namespace, + LocalAuthority: c.JWTAuthLocal, + Debug: c.JWTAuthDebug, + } + + return auth.NewJWTAuthMiddleware(authOpts, next) +} + func printVersion() { sha := "unknown" if len(GitCommit) > 0 { @@ -474,3 +495,26 @@ func (nc *WriterCounter) Write(p []byte) (int, error) { nc.bytes += int64(n) return n, err } + +func getFnName() (string, error) { + name, ok := os.LookupEnv("OPENFAAS_NAME") + if !ok || len(name) == 0 { + return "", fmt.Errorf("env variable 'OPENFAAS_NAME' not set") + } + + return name, nil +} + +// getFnNamespace gets the namespace name from the env variable OPENFAAS_NAMESPACE +// or reads it from the service account if the env variable is not present +func getFnNamespace() (string, error) { + if namespace, ok := os.LookupEnv("OPENFAAS_NAMESPACE"); ok { + return namespace, nil + } + + nsVal, err := os.ReadFile("/var/run/secrets/kubernetes.io/serviceaccount/namespace") + if err != nil { + return "", err + } + return string(nsVal), nil +} diff --git a/vendor/github.com/openfaas/faas-middleware/auth/jwt_authenticator.go b/vendor/github.com/openfaas/faas-middleware/auth/jwt_authenticator.go new file mode 100644 index 00000000..04daa459 --- /dev/null +++ b/vendor/github.com/openfaas/faas-middleware/auth/jwt_authenticator.go @@ -0,0 +1,293 @@ +package auth + +import ( + "encoding/json" + "fmt" + "io" + "log" + "net/http" + "regexp" + "strings" + "time" + + "github.com/golang-jwt/jwt/v5" + "github.com/rakutentech/jwk-go/jwk" +) + +const ( + authorityURL = "http://gateway.openfaas:8080/.well-known/openid-configuration" + localAuthorityURL = "http://127.0.0.1:8000/.well-known/openid-configuration" + functionRealm = "IAM function invoke" +) + +type jwtAuth struct { + next http.Handler + opts JWTAuthOptions + + keySet jwk.KeySpecSet + issuer string +} + +// JWTAuthOptions stores the configuration for JWT based function authentication +type JWTAuthOptions struct { + Name string + Namespace string + LocalAuthority bool + Debug bool +} + +func (a jwtAuth) ServeHTTP(w http.ResponseWriter, r *http.Request) { + issuer := a.issuer + + st := time.Now() + for _, key := range a.keySet.Keys { + log.Printf("%s: %v", issuer, key.KeyID) + } + + var bearer string + if v := r.Header.Get("Authorization"); v != "" { + bearer = strings.TrimPrefix(v, "Bearer ") + } + + if bearer == "" { + writeUnauthorized(w, "Bearer must be present in Authorization header") + log.Printf("%s %s - %d ACCESS DENIED - (%s)", r.Method, r.URL.Path, http.StatusUnauthorized, time.Since(st).Round(time.Millisecond)) + return + } + + parseOptions := []jwt.ParserOption{ + jwt.WithIssuer(issuer), + // The OpenFaaS gateway is the expected audience but we can use the issuer url + // since the gateway is also the issuer of function tokens and thus has the same url. + jwt.WithAudience(issuer), + jwt.WithLeeway(time.Second * 1), + } + + functionClaims := FunctionClaims{} + token, err := jwt.ParseWithClaims(bearer, &functionClaims, func(token *jwt.Token) (interface{}, error) { + if a.opts.Debug { + log.Printf("[JWT Auth] Token: audience: %v\tissuer: %v", functionClaims.Audience, functionClaims.Issuer) + } + + kid, ok := token.Header["kid"].(string) + if !ok { + return nil, fmt.Errorf("invalid kid: %v", token.Header["kid"]) + } + + // HV: Consider caching and refreshing the keyset to handle key rotations. + var key *jwk.KeySpec + for _, k := range a.keySet.Keys { + if k.KeyID == kid { + key = &k + break + } + } + + if key == nil { + return nil, fmt.Errorf("invalid kid: %s", kid) + } + return key.Key, nil + }, parseOptions...) + if err != nil { + writeUnauthorized(w, fmt.Sprintf("failed to parse JWT token: %s", err)) + log.Printf("%s %s - %d ACCESS DENIED - (%s)", r.Method, r.URL.Path, http.StatusUnauthorized, time.Since(st).Round(time.Millisecond)) + return + } + + if !token.Valid { + writeUnauthorized(w, fmt.Sprintf("invalid JWT token: %s", bearer)) + + log.Printf("%s %s - %d ACCESS DENIED - (%s)", r.Method, r.URL.Path, http.StatusUnauthorized, time.Since(st).Round(time.Millisecond)) + return + } + + if !isAuthorized(functionClaims.Authentication, a.opts.Namespace, a.opts.Name) { + w.Header().Set("X-OpenFaaS-Internal", "faas-middleware") + http.Error(w, "insufficient permissions", http.StatusForbidden) + + log.Printf("%s %s - %d ACCESS DENIED - (%s)", r.Method, r.URL.Path, http.StatusForbidden, time.Since(st).Round(time.Millisecond)) + return + } + + a.next.ServeHTTP(w, r) +} + +// NewJWTAuthMiddleware creates a new middleware handler to handle authentication with OpenFaaS function +// access tokens. +func NewJWTAuthMiddleware(opts JWTAuthOptions, next http.Handler) (http.Handler, error) { + authority := authorityURL + if opts.LocalAuthority { + authority = localAuthorityURL + } + + config, err := getConfig(authority) + if err != nil { + return nil, err + } + + if opts.Debug { + log.Printf("[JWT Auth] Issuer: %s\tJWKS URI: %s", config.Issuer, config.JWKSURI) + } + + keySet, err := getKeyset(config.JWKSURI) + if err != nil { + return nil, err + } + + if opts.Debug { + for _, key := range keySet.Keys { + log.Printf("[JWT Auth] Key: %s", key.KeyID) + } + } + + return jwtAuth{ + next: next, + opts: opts, + keySet: keySet, + issuer: config.Issuer, + }, nil +} + +// writeUnauthorized replies to the request with the specified error message and 401 HTTP code. +// It sets the WWW-Authenticate header. +// It does not otherwise end the request; the caller should ensure no further writes are done to w. +// The error message should be plain text. +func writeUnauthorized(w http.ResponseWriter, err string) { + w.Header().Set("X-OpenFaaS-Internal", "faas-middleware") + w.Header().Set("WWW-Authenticate", fmt.Sprintf("Bearer realm=%s", functionRealm)) + http.Error(w, err, http.StatusUnauthorized) +} + +func getKeyset(uri string) (jwk.KeySpecSet, error) { + var set jwk.KeySpecSet + req, err := http.NewRequest(http.MethodGet, uri, nil) + if err != nil { + return set, err + } + + req.Header.Add("User-Agent", "openfaas-watchdog") + + res, err := http.DefaultClient.Do(req) + if err != nil { + return set, err + } + + var body []byte + + if res.Body != nil { + defer res.Body.Close() + body, _ = io.ReadAll(res.Body) + } + + if res.StatusCode != http.StatusOK { + return set, fmt.Errorf("failed to get keyset from %s, status code: %d, body: %s", uri, res.StatusCode, string(body)) + } + + if err := json.Unmarshal(body, &set); err != nil { + return set, err + } + + return set, nil +} + +func getConfig(jwksURL string) (openIDConfiguration, error) { + var config openIDConfiguration + + req, err := http.NewRequest(http.MethodGet, jwksURL, nil) + if err != nil { + return config, err + } + + res, err := http.DefaultClient.Do(req) + if err != nil { + return config, err + } + + var body []byte + if res.Body != nil { + defer res.Body.Close() + body, _ = io.ReadAll(res.Body) + } + + if res.StatusCode != http.StatusOK { + return config, fmt.Errorf("failed to get config from %s, status code: %d, body: %s", jwksURL, res.StatusCode, string(body)) + } + + if err := json.Unmarshal(body, &config); err != nil { + return config, err + } + + return config, nil +} + +type openIDConfiguration struct { + Issuer string `json:"issuer"` + JWKSURI string `json:"jwks_uri"` +} + +type FunctionClaims struct { + jwt.RegisteredClaims + + Authentication AuthPermissions `json:"function"` +} + +type AuthPermissions struct { + Permissions []string `json:"permissions"` + Audience []string `json:"audience,omitempty"` +} + +func isAuthorized(auth AuthPermissions, namespace, fn string) bool { + functionRef := fmt.Sprintf("%s:%s", namespace, fn) + + return matchResource(auth.Audience, functionRef, false) && + matchResource(auth.Permissions, functionRef, true) +} + +// matchResources checks if ref matches one of the resources. +// The function will return true if a match is found. +// If required is false, this function will return true if a match is found or the resource list is empty. +func matchResource(resources []string, ref string, req bool) bool { + if !req { + if len(resources) == 0 { + return true + } + } + + for _, res := range resources { + if res == "*" { + return true + } + + if matchString(res, ref) { + return true + } + } + + return false +} + +func matchString(pattern string, value string) bool { + if len(pattern) > 0 { + result, _ := regexp.MatchString(wildCardToRegexp(pattern), value) + return result + } + + return pattern == value +} + +// wildCardToRegexp converts a wildcard pattern to a regular expression pattern. +func wildCardToRegexp(pattern string) string { + var result strings.Builder + for i, literal := range strings.Split(pattern, "*") { + + // Replace * with .* + if i > 0 { + result.WriteString(".*") + } + + // Quote any regular expression meta characters in the + // literal text. + result.WriteString(regexp.QuoteMeta(literal)) + } + return result.String() +} diff --git a/vendor/modules.txt b/vendor/modules.txt index 8034306a..7ee9259e 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -13,8 +13,9 @@ github.com/docker/go-units # github.com/golang-jwt/jwt/v5 v5.2.1 ## explicit; go 1.18 github.com/golang-jwt/jwt/v5 -# github.com/openfaas/faas-middleware v1.2.3 +# github.com/openfaas/faas-middleware v1.2.3 => /home/welteki/code/openfaas/oss/faas-middleware ## explicit; go 1.20 +github.com/openfaas/faas-middleware/auth github.com/openfaas/faas-middleware/concurrency-limiter # github.com/openfaas/faas-provider v0.25.3 ## explicit; go 1.20 @@ -92,3 +93,4 @@ google.golang.org/protobuf/reflect/protoregistry google.golang.org/protobuf/runtime/protoiface google.golang.org/protobuf/runtime/protoimpl google.golang.org/protobuf/types/known/timestamppb +# github.com/openfaas/faas-middleware => /home/welteki/code/openfaas/oss/faas-middleware