From 8957a5ad8b9f4b33b11c146755a7abb468470628 Mon Sep 17 00:00:00 2001 From: Eguzki Astiz Lezaun Date: Tue, 28 Nov 2023 18:18:41 +0100 Subject: [PATCH] kuadrant authpolicy command: support apikey --- .../petstore-multiple-sec-requirements.yaml | 32 +++- pkg/kuadrantapi/authpolicy.go | 147 ++++++++++++++---- pkg/utils/maps.go | 12 ++ 3 files changed, 152 insertions(+), 39 deletions(-) create mode 100644 pkg/utils/maps.go diff --git a/examples/oas3/petstore-multiple-sec-requirements.yaml b/examples/oas3/petstore-multiple-sec-requirements.yaml index 7afe0ff..91b2184 100644 --- a/examples/oas3/petstore-multiple-sec-requirements.yaml +++ b/examples/oas3/petstore-multiple-sec-requirements.yaml @@ -3,6 +3,15 @@ openapi: "3.1.0" info: title: "Pet Store API" version: "1.0.0" + x-kuadrant: + route: + name: "petstore" + namespace: "petstore" + hostnames: + - example.com + parentRefs: + - name: istio-ingressgateway + namespace: istio-system servers: - url: https://toplevel.example.io/v1 paths: @@ -15,7 +24,7 @@ paths: post: # API key operationId: "postCat" security: - - petstore_api_key: [] + - cat_api_key: [] responses: 405: description: "invalid input" @@ -23,17 +32,30 @@ paths: get: # OIDC operationId: "getDog" security: - - petstore_oidc: + - oidc: - read:dogs responses: 405: description: "invalid input" + /snake: + get: # OIDC or API key + operationId: "getSnake" + security: + - oidc: ["read:snakes"] + - snakes_api_key: [] + responses: + 405: + description: "invalid input" components: securitySchemes: - petstore_api_key: + cat_api_key: type: apiKey name: api_key in: header - petstore_oidc: + oidc: type: openIdConnect - openIdConnectUrl: http://example.org/auth/realms/myrealm + openIdConnectUrl: https://example.com/.well-known/openid-configuration + snakes_api_key: + type: apiKey + name: snake_token + in: query diff --git a/pkg/kuadrantapi/authpolicy.go b/pkg/kuadrantapi/authpolicy.go index b302edd..07457b6 100644 --- a/pkg/kuadrantapi/authpolicy.go +++ b/pkg/kuadrantapi/authpolicy.go @@ -1,6 +1,9 @@ package kuadrantapi import ( + "errors" + "fmt" + "github.com/getkin/kin-openapi/openapi3" authorinoapi "github.com/kuadrant/authorino/api/v1beta2" kuadrantapiv1beta2 "github.com/kuadrant/kuadrant-operator/api/v1beta2" @@ -12,6 +15,10 @@ import ( "github.com/kuadrant/kuadrantctl/pkg/utils" ) +const ( + APIKeySecretLabel = "kuadrant.io/apikeys-by" +) + func AuthPolicyObjectMetaFromOAS(doc *openapi3.T) metav1.ObjectMeta { return gatewayapi.HTTPRouteObjectMetaFromOAS(doc) } @@ -122,27 +129,10 @@ func AuthPolicyAuthenticationSchemeFromOAS(doc *openapi3.T) map[string]kuadranta kuadrantPathExtension.GetPathMatchType(), ) - oidcScheme := findOIDCSecuritySchemesFromRequirements(doc, secRequirements) - - if oidcScheme == nil { - // no oidc sec scheme found - continue - } - - authName := utils.OpenAPIOperationName(path, verb, operation) + operationAuthentication := buildOperationAuthentication(doc, basePath, path, pathItem, verb, operation, pathMatchType, secRequirements) - authentication[authName] = kuadrantapiv1beta2.AuthenticationSpec{ - CommonAuthRuleSpec: kuadrantapiv1beta2.CommonAuthRuleSpec{ - RouteSelectors: buildAuthPolicyRouteSelectors(basePath, path, pathItem, verb, operation, pathMatchType), - }, - AuthenticationSpec: authorinoapi.AuthenticationSpec{ - AuthenticationMethodSpec: authorinoapi.AuthenticationMethodSpec{ - Jwt: &authorinoapi.JwtAuthenticationSpec{ - IssuerUrl: oidcScheme.OpenIdConnectUrl, - }, - }, - }, - } + // Aggregate auth methods per operation + authentication = utils.MergeMaps(authentication, operationAuthentication) } } @@ -153,23 +143,112 @@ func AuthPolicyAuthenticationSchemeFromOAS(doc *openapi3.T) map[string]kuadranta return authentication } -func findOIDCSecuritySchemesFromRequirements(doc *openapi3.T, secRequirements openapi3.SecurityRequirements) *openapi3.SecurityScheme { +func buildOperationAuthentication(doc *openapi3.T, basePath, path string, pathItem *openapi3.PathItem, verb string, op *openapi3.Operation, pathMatchType gatewayapiv1beta1.PathMatchType, secRequirements openapi3.SecurityRequirements) map[string]kuadrantapiv1beta2.AuthenticationSpec { + // OpenAPI supports as security requirement to have multiple security schemes and ALL + // of the must be satisfied. + // From https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#security-requirement-object + // Kuadrant does not support it yet: https://github.com/Kuadrant/authorino/issues/112 + // not supported (AND'ed) + // security: + // - petstore_api_key: [] + // petstore_oidc: [] + // supported (OR'ed) + // security: + // - petstore_api_key: [] + // - petstore_oidc: [] + + opAuth := make(map[string]kuadrantapiv1beta2.AuthenticationSpec, 0) for _, secReq := range secRequirements { - for secReqItemName := range secReq { - secScheme, ok := doc.Components.SecuritySchemes[secReqItemName] - if !ok { - // should never happen. OpenAPI validation should detect this issue - continue - } - if secScheme == nil || secScheme.Value == nil { - continue - } - // Ref https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#fixed-fields-23 - if secScheme.Value.Type == "openIdConnect" { - return secScheme.Value + if len(secReq) > 1 { + panic(errors.New("multiple schemes that require ALL must be satisfied, currently not supported")) + } + + extractSecReqItemName := func(sr openapi3.SecurityRequirement) string { + for secReqItemName := range sr { + return secReqItemName } + + return "" + } + + secReqItemName := extractSecReqItemName(secReq) + + secScheme, ok := doc.Components.SecuritySchemes[secReqItemName] + if !ok { + // should never happen. OpenAPI validation should detect this issue + continue + } + + if secScheme == nil || secScheme.Value == nil { + continue } + + authName := fmt.Sprintf("%s_%s", utils.OpenAPIOperationName(path, verb, op), secReqItemName) + + // Ref https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#fixed-fields-23 + switch secScheme.Value.Type { + case "openIdConnect": + opAuth[authName] = openIDAuthenticationSpec(basePath, path, pathItem, verb, op, pathMatchType, *secScheme.Value) + case "apiKey": + opAuth[authName] = apiKeyAuthenticationSpec(basePath, path, pathItem, verb, op, pathMatchType, secReqItemName, *secScheme.Value) + } + } + + if len(opAuth) == 0 { + return nil } - return nil + return opAuth +} + +func apiKeyAuthenticationSpec(basePath, path string, pathItem *openapi3.PathItem, verb string, op *openapi3.Operation, pathMatchType gatewayapiv1beta1.PathMatchType, secSchemeName string, secScheme openapi3.SecurityScheme) kuadrantapiv1beta2.AuthenticationSpec { + // From https://github.com/Kuadrant/kuadrantctl/pull/46#issuecomment-1830278191 + // secScheme.In is required + // secScheme.Name is required + credentials := authorinoapi.Credentials{} + switch secScheme.In { + case "query": + credentials.QueryString = &authorinoapi.Named{Name: secScheme.Name} + case "header": + credentials.CustomHeader = &authorinoapi.CustomHeader{ + Named: authorinoapi.Named{Name: secScheme.Name}, + } + case "cookie": + credentials.Cookie = &authorinoapi.Named{Name: secScheme.Name} + } + + return kuadrantapiv1beta2.AuthenticationSpec{ + CommonAuthRuleSpec: kuadrantapiv1beta2.CommonAuthRuleSpec{ + RouteSelectors: buildAuthPolicyRouteSelectors(basePath, path, pathItem, verb, op, pathMatchType), + }, + AuthenticationSpec: authorinoapi.AuthenticationSpec{ + Credentials: credentials, + AuthenticationMethodSpec: authorinoapi.AuthenticationMethodSpec{ + ApiKey: &authorinoapi.ApiKeyAuthenticationSpec{ + Selector: &metav1.LabelSelector{ + MatchLabels: map[string]string{ + // label selector be like + // kuadrant.io/apikeys-by: ${SecuritySchemeName} + APIKeySecretLabel: secSchemeName, + }, + }, + }, + }, + }, + } +} + +func openIDAuthenticationSpec(basePath, path string, pathItem *openapi3.PathItem, verb string, op *openapi3.Operation, pathMatchType gatewayapiv1beta1.PathMatchType, secScheme openapi3.SecurityScheme) kuadrantapiv1beta2.AuthenticationSpec { + return kuadrantapiv1beta2.AuthenticationSpec{ + CommonAuthRuleSpec: kuadrantapiv1beta2.CommonAuthRuleSpec{ + RouteSelectors: buildAuthPolicyRouteSelectors(basePath, path, pathItem, verb, op, pathMatchType), + }, + AuthenticationSpec: authorinoapi.AuthenticationSpec{ + AuthenticationMethodSpec: authorinoapi.AuthenticationMethodSpec{ + Jwt: &authorinoapi.JwtAuthenticationSpec{ + IssuerUrl: secScheme.OpenIdConnectUrl, + }, + }, + }, + } } diff --git a/pkg/utils/maps.go b/pkg/utils/maps.go new file mode 100644 index 0000000..c423dc4 --- /dev/null +++ b/pkg/utils/maps.go @@ -0,0 +1,12 @@ +package utils + +func MergeMaps[K comparable, V any](MyMap1 map[K]V, MyMap2 map[K]V) map[K]V { + merged := make(map[K]V) + for key, val := range MyMap1 { + merged[key] = val + } + for key, val := range MyMap2 { + merged[key] = val + } + return merged +}