diff --git a/.github/workflows/e2e-test.yaml b/.github/workflows/e2e-test.yaml index d8196391..e49f29ac 100644 --- a/.github/workflows/e2e-test.yaml +++ b/.github/workflows/e2e-test.yaml @@ -15,6 +15,7 @@ jobs: matrix: go-version: [1.19.x] platform: [ubuntu-latest] + authconfig_version: [v1beta1, v1beta2] runs-on: ${{ matrix.platform }} defaults: run: @@ -34,5 +35,6 @@ jobs: - name: Run make e2e env: OPERATOR_VERSION: ${{ github.event.inputs.operatorVersion }} + AUTHCONFIG_VERSION: ${{ matrix.authconfig_version }} run: | make e2e diff --git a/.github/workflows/integration-test.yaml b/.github/workflows/integration-test.yaml index 6822362b..545a9d47 100644 --- a/.github/workflows/integration-test.yaml +++ b/.github/workflows/integration-test.yaml @@ -31,15 +31,9 @@ jobs: run: go install sigs.k8s.io/kind@v0.20.0 - name: Create kind cluster run: kind create cluster --name authorino-smoke-tests - - name: Install cert-manager - run: | - kubectl apply -f https://github.com/jetstack/cert-manager/releases/download/v1.4.0/cert-manager.yaml - kubectl delete mutatingwebhookconfiguration.admissionregistration.k8s.io/cert-manager-webhook - kubectl delete validatingwebhookconfigurations.admissionregistration.k8s.io/cert-manager-webhook - kubectl -n cert-manager wait --timeout=300s --for=condition=Available deployments --all - name: Install Authorino Operator run: | - kubectl apply -f https://raw.githubusercontent.com/Kuadrant/authorino-operator/main/config/deploy/manifests.yaml + curl -sL https://raw.githubusercontent.com/Kuadrant/authorino-operator/main/utils/install.sh | bash -s kubectl -n authorino-operator wait --timeout=300s --for=condition=Available deployments --all - name: Create the namespace run: | @@ -79,7 +73,7 @@ jobs: - name: Run e2e tests env: NAMESPACE: authorino - AUTHCONFIG: https://raw.githubusercontent.com/Kuadrant/authorino/main/tests/authconfig.yaml - AUTHCONFIG_INVALID: https://raw.githubusercontent.com/Kuadrant/authorino/main/tests/authconfig-invalid.yaml + AUTHCONFIG: https://raw.githubusercontent.com/Kuadrant/authorino/main/tests/v1beta2/authconfig.yaml + AUTHCONFIG_INVALID: https://raw.githubusercontent.com/Kuadrant/authorino/main/tests/v1beta2/authconfig-invalid.yaml run: | curl -sSL https://raw.githubusercontent.com/Kuadrant/authorino/main/tests/e2e-test.sh | bash diff --git a/Makefile b/Makefile index 17205753..c2f62680 100644 --- a/Makefile +++ b/Makefile @@ -109,7 +109,8 @@ generate: vendor controller-gen ## Generates types deepcopy code $(MAKE) fmt vet manifests: controller-gen kustomize ## Generates the manifests in $PROJECT_DIR/install - controller-gen crd:crdVersions=v1 rbac:roleName=manager-role webhook paths="./..." output:crd:artifacts:config=install/crd output:rbac:artifacts:config=install/rbac && kustomize build install > $(AUTHORINO_MANIFESTS) + controller-gen crd:crdVersions=v1 rbac:roleName=manager-role webhook paths="./..." output:crd:artifacts:config=install/crd output:rbac:artifacts:config=install/rbac && $(KUSTOMIZE) build install > $(AUTHORINO_MANIFESTS) + $(MAKE) patch-webhook run: generate manifests ## Runs the application against the Kubernetes cluster configured in ~/.kube/config go run -ldflags "-X main.version=$(VERSION)" ./main.go server @@ -143,22 +144,15 @@ report-benchmarks: cover: ## Shows test coverage go tool cover -html=cover.out +AUTHCONFIG_VERSION ?= v1beta2 VERBOSE ?= 0 e2e: ## Runs the end-to-end tests on a local environment setup $(MAKE) local-setup NAMESPACE=authorino KIND_CLUSTER_NAME=authorino-e2e AUTHORINO_IMAGE=$(AUTHORINO_IMAGE) TLS_ENABLED=$(TLS_ENABLED) OPERATOR_BRANCH=$(OPERATOR_BRANCH) AUTHORINO_MANIFESTS=$(AUTHORINO_MANIFESTS) AUTHORINO_INSTANCE=$(AUTHORINO_INSTANCE) ENVOY_OVERLAY=$(ENVOY_OVERLAY) DEPLOY_KEYCLOAK=1 FF=1 - NAMESPACE=authorino VERBOSE=$(VERBOSE) ./tests/e2e-test.sh + NAMESPACE=authorino AUTHCONFIG_VERSION=$(AUTHCONFIG_VERSION) VERBOSE=$(VERBOSE) ./tests/e2e-test.sh ##@ Apps -.PHONY: cert-manager user-apps keycloak dex limitador - -cert-manager: ## Installs CertManager into the Kubernetes cluster configured in ~/.kube/config -ifeq (true,$(TLS_ENABLED)) - kubectl apply -f https://github.com/jetstack/cert-manager/releases/download/v1.4.0/cert-manager.yaml - kubectl delete mutatingwebhookconfiguration.admissionregistration.k8s.io/cert-manager-webhook - kubectl delete validatingwebhookconfigurations.admissionregistration.k8s.io/cert-manager-webhook - kubectl -n cert-manager wait --timeout=300s --for=condition=Available deployments --all -endif +.PHONY: user-apps keycloak dex limitador DEPLOY_KEYCLOAK ?= $(DEPLOY_IDPS) DEPLOY_DEX ?= $(DEPLOY_IDPS) @@ -188,16 +182,19 @@ limitador: ## Deploys Limitador from kuadrant/authorino-examples into the Kubern ##@ Installation -.PHONY: install-operator uninstall-operator install uninstall +.PHONY: install-operator uninstall-operator install uninstall patch-webhook + +AUTHORINO_OPERATOR_NAMESPACE ?= authorino-operator ifeq (latest,$(OPERATOR_VERSION)) OPERATOR_BRANCH = main else OPERATOR_BRANCH = $(OPERATOR_VERSION) endif -install-operator: ## Installs Authorino Operator and corresponding version of the manifests into the Kubernetes cluster configured in ~/.kube/config - kubectl apply -f https://raw.githubusercontent.com/Kuadrant/authorino-operator/$(OPERATOR_BRANCH)/config/deploy/manifests.yaml - kubectl -n authorino-operator wait --timeout=300s --for=condition=Available deployments --all +install-operator: ## Installs Authorino Operator and dependencies into the Kubernetes cluster configured in ~/.kube/config + curl -sL https://raw.githubusercontent.com/Kuadrant/authorino-operator/$(OPERATOR_BRANCH)/utils/install.sh | bash -s -- --git-ref $(OPERATOR_BRANCH) + kubectl patch deployment/authorino-webhooks -n $(AUTHORINO_OPERATOR_NAMESPACE) -p '{"spec":{"template":{"spec":{"containers":[{"name":"webhooks","image":"$(AUTHORINO_IMAGE)","imagePullPolicy":"IfNotPresent"}]}}}}' + kubectl -n $(AUTHORINO_OPERATOR_NAMESPACE) wait --timeout=300s --for=condition=Available deployments --all uninstall-operator: ## Uninstalls Authorino Operator and corresponding version of the manifests from the Kubernetes cluster configured in ~/.kube/config kubectl delete -f https://raw.githubusercontent.com/Kuadrant/authorino-operator/$(OPERATOR_BRANCH)/config/deploy/manifests.yaml @@ -208,6 +205,13 @@ install: manifests ## Installs the current manifests (CRD, RBAC) into the Kubern uninstall: manifests ## Uninstalls the current manifests (CRD, RBAC) from the Kubernetes cluster configured in ~/.kube/config kubectl delete -f $(AUTHORINO_MANIFESTS) +patch-webhook: export WEBHOOK_NAMESPACE=$(AUTHORINO_OPERATOR_NAMESPACE) +patch-webhook: + envsubst \ + < $(AUTHORINO_MANIFESTS) \ + > $(AUTHORINO_MANIFESTS).tmp && \ + mv $(AUTHORINO_MANIFESTS).tmp $(AUTHORINO_MANIFESTS) + ##@ Deployment .PHONY: namespace certs deploy @@ -250,7 +254,7 @@ cluster: kind ## Starts a local Kubernetes cluster using Kind local-build: kind docker-build ## Builds an image based on the current branch and pushes it to the registry into the local Kubernetes cluster started with Kind $(KIND) load docker-image $(AUTHORINO_IMAGE) --name $(KIND_CLUSTER_NAME) -local-setup: cluster local-build cert-manager install-operator install namespace deploy user-apps ## Sets up a test/dev local Kubernetes server using Kind, loaded up with a freshly built Authorino image and apps +local-setup: cluster local-build install-operator install namespace deploy user-apps ## Sets up a test/dev local Kubernetes server using Kind, loaded up with a freshly built Authorino image and apps kubectl -n $(NAMESPACE) wait --timeout=300s --for=condition=Available deployments --all @{ \ echo "Now you can export the envoy service by doing:"; \ diff --git a/PROJECT b/PROJECT index 1c1b6fd9..1fd76fe1 100644 --- a/PROJECT +++ b/PROJECT @@ -6,6 +6,9 @@ resources: - group: config kind: AuthConfig version: v1beta1 +- group: config + kind: AuthConfig + version: v1beta2 version: 3-alpha plugins: go.sdk.operatorframework.io/v2-alpha: {} diff --git a/README.md b/README.md index af2c6350..c7b795f7 100644 --- a/README.md +++ b/README.md @@ -287,7 +287,7 @@ For a detailed description of the features above, refer to the [Features](./docs
Can't I just use Envoy JWT Authentication and RBAC filters? - Envoy's [JWT Authentication](https://www.envoyproxy.io/docs/envoy/latest/api-v3/extensions/filters/http/jwt_authn/v3/config.proto.html) works pretty much similar to Authorino's [JOSE/JWT verification and validation for OpenID Connect](./docs/features.md#openid-connect-oidc-jwtjose-verification-and-validation-identityoidc). In both cases, the JSON Web Key Sets (JWKS) to verify the JWTs are auto-loaded and cached to be used in request-time. Moreover, you can configure for details such as where to extract the JWT from the HTTP request (header, param or cookie) and do some cool tricks regarding how dynamic metadata based on JWT claims can be injected to consecutive filters in the chain. + Envoy's [JWT Authentication](https://www.envoyproxy.io/docs/envoy/latest/api-v3/extensions/filters/http/jwt_authn/v3/config.proto.html) works pretty much similar to Authorino's [JOSE/JWT verification and validation for OpenID Connect](./docs/features.md#jwt-verification-authenticationjwt). In both cases, the JSON Web Key Sets (JWKS) to verify the JWTs are auto-loaded and cached to be used in request-time. Moreover, you can configure for details such as where to extract the JWT from the HTTP request (header, param or cookie) and do some cool tricks regarding how dynamic metadata based on JWT claims can be injected to consecutive filters in the chain. However, in terms of authorization, while Envoy's implementation essentially allows to check for the list of audiences (`aud` JWT claim), Authorino opens up for a lot more options such as pattern-matching rules with operators and conditionals, built-in OPA and other methods of evaluating authorization policies. @@ -307,7 +307,7 @@ For a detailed description of the features above, refer to the [Features](./docs Authorino is an Envoy-compatible external authorization service. One can use Authorino with or without Istio. - In particular, [Istio Authorization Policies](https://istio.io/latest/docs/reference/config/security/authorization-policy/) can be seen, in terms of functionality and expressiveness, as a subset of one type of authorization policies supported by Authorino, the [JSON pattern-matching authorization](./docs/features.md#json-pattern-matching-authorization-rules-authorizationjson) policies. While Istio, however, is heavily focused on specific use cases of API Management, offering a relatively limited list of [supported attribute conditions](https://istio.io/latest/docs/reference/config/security/conditions/), Authorino is more generic, allowing to express authorization rules for a wider spectrum of use cases – ACLs, RBAC, ABAC, etc, pretty much counting on any attribute of the Envoy payload, identity object and external metadata available. + In particular, [Istio Authorization Policies](https://istio.io/latest/docs/reference/config/security/authorization-policy/) can be seen, in terms of functionality and expressiveness, as a subset of one type of authorization policies supported by Authorino, the [pattern-matching authorization](./docs/features.md#pattern-matching-authorization-authorizationpatternmatching) policies. While Istio, however, is heavily focused on specific use cases of API Management, offering a relatively limited list of [supported attribute conditions](https://istio.io/latest/docs/reference/config/security/conditions/), Authorino is more generic, allowing to express authorization rules for a wider spectrum of use cases – ACLs, RBAC, ABAC, etc, pretty much counting on any attribute of the Envoy payload, identity object and external metadata available. Authorino also provides built-in OPA authorization, several other methods of authentication and identity verification (e.g. Kubernetes token validation, API key-based authentication, OAuth token introspection, OIDC-discoverable JWT verification, etc), and features like fetching of external metadata (HTTP services, OIDC userinfo, UMA resource data), token normalization, wristband tokens and dynamic responses. These all can be used independently or combined, in a simple and straightforward Kubernetes-native fashion. @@ -327,7 +327,7 @@ For a detailed description of the features above, refer to the [Features](./docs No, you do not. However, if you are comfortable with [Rego](https://www.openpolicyagent.org/docs/latest/policy-language/) from Open Policy Agent (OPA), there are some quite interesting things you can do in Authorino, just as you would in any OPA server or OPA plugin, but leveraging Authorino's [built-in OPA module](./docs/features.md#open-policy-agent-opa-rego-policies-authorizationopa) instead. Authorino's OPA module is compiled as part of Authorino's code directly from the Golang packages, and imposes no extra latency to the evaluation of your authorization policies. Even the policies themselves are pre-compiled in reconciliation-time, for fast evaluation afterwards, in request-time. - On the other hand, if you do not want to learn Rego or in any case would like to combine it with declarative and Kubernetes-native authN/authZ spec for your services, Authorino does complement OPA with at least two other methods for expressing authorization policies – i.e. [JSON pattern-matching authorization rules](./docs/features.md#json-pattern-matching-authorization-rules-authorizationjson) and [Kubernetes SubjectAccessReview](./docs/features.md#kubernetes-subjectaccessreview-authorizationkubernetes), the latter allowing to rely completely on the Kubernetes RBAC. + On the other hand, if you do not want to learn Rego or in any case would like to combine it with declarative and Kubernetes-native authN/authZ spec for your services, Authorino does complement OPA with at least two other methods for expressing authorization policies – i.e. [pattern-matching authorization](./docs/features.md#pattern-matching-authorization-authorizationpatternmatching) and [Kubernetes SubjectAccessReview](./docs/features.md#kubernetes-subjectaccessreview-authorizationkubernetessubjectaccessreview), the latter allowing to rely completely on the Kubernetes RBAC. You break down, mix and combine these methods and technolgies in as many authorization policies as you want, potentially applying them according to specific conditions. Authorino will trigger the evaluation of concurrent policies in parallel, aborting the context if any of the processes denies access. diff --git a/api/v1beta1/auth_config_conversion.go b/api/v1beta1/auth_config_conversion.go new file mode 100644 index 00000000..6b810a0f --- /dev/null +++ b/api/v1beta1/auth_config_conversion.go @@ -0,0 +1,4 @@ +package v1beta1 + +// Hub marks this version as a conversion hub. +func (a *AuthConfig) Hub() {} diff --git a/api/v1beta1/auth_config_types.go b/api/v1beta1/auth_config_types.go index 1e975ebf..f0535d99 100644 --- a/api/v1beta1/auth_config_types.go +++ b/api/v1beta1/auth_config_types.go @@ -774,6 +774,7 @@ func (s *AuthConfigStatus) Ready() bool { // AuthConfig is the schema for Authorino's AuthConfig API // +kubebuilder:object:root=true // +kubebuilder:subresource:status +// +kubebuilder:storageversion // +kubebuilder:printcolumn:name="Ready",type=string,JSONPath=`.status.summary.ready`,description="Ready for all hosts" // +kubebuilder:printcolumn:name="Hosts",type=string,JSONPath=`.status.summary.numHostsReady`,description="Number of hosts ready" // +kubebuilder:printcolumn:name="Authentication",type=integer,JSONPath=`.status.summary.numIdentitySources`,description="Number of trusted identity sources",priority=2 diff --git a/api/v1beta2/auth_config_conversion.go b/api/v1beta2/auth_config_conversion.go new file mode 100644 index 00000000..d0acb1de --- /dev/null +++ b/api/v1beta2/auth_config_conversion.go @@ -0,0 +1,1046 @@ +package v1beta2 + +import ( + "fmt" + + "github.com/kuadrant/authorino/api/v1beta1" + "github.com/kuadrant/authorino/pkg/utils" + + "github.com/tidwall/gjson" + k8sruntime "k8s.io/apimachinery/pkg/runtime" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/conversion" +) + +func (src *AuthConfig) ConvertTo(dstRaw conversion.Hub) error { + dst := dstRaw.(*v1beta1.AuthConfig) + + logger := ctrl.Log.WithName("webhook").WithName("authconfig").WithName("converto").WithValues("src", src) + logger.V(1).Info("starting converting resource") + + // metadata + dst.ObjectMeta = src.ObjectMeta + + // hosts + dst.Spec.Hosts = src.Spec.Hosts + + // named patterns + if src.Spec.NamedPatterns != nil { + dst.Spec.Patterns = make(map[string]v1beta1.JSONPatternExpressions, len(src.Spec.NamedPatterns)) + for name, patterns := range src.Spec.NamedPatterns { + dst.Spec.Patterns[name] = utils.Map(patterns, convertPatternExpressionTo) + } + } + + // conditions + dst.Spec.Conditions = utils.Map(src.Spec.Conditions, convertPatternExpressionOrRefTo) + + // identity + for name, authentication := range src.Spec.Authentication { + identity := convertAuthenticationTo(name, authentication) + dst.Spec.Identity = append(dst.Spec.Identity, identity) + } + + // metadata + for name, metadataSrc := range src.Spec.Metadata { + metadata := convertMetadataTo(name, metadataSrc) + dst.Spec.Metadata = append(dst.Spec.Metadata, metadata) + } + + // authorization + for name, authorizationSrc := range src.Spec.Authorization { + authorization := convertAuthorizationTo(name, authorizationSrc) + dst.Spec.Authorization = append(dst.Spec.Authorization, authorization) + } + + // response + if src.Spec.Response != nil { + for name, responseSrc := range src.Spec.Response.Success.Headers { + response := convertSuccessResponseTo(name, responseSrc.SuccessResponseSpec, "httpHeader") + dst.Spec.Response = append(dst.Spec.Response, response) + } + + for name, responseSrc := range src.Spec.Response.Success.DynamicMetadata { + response := convertSuccessResponseTo(name, responseSrc, "envoyDynamicMetadata") + dst.Spec.Response = append(dst.Spec.Response, response) + } + + // denyWith + if src.Spec.Response.Unauthenticated != nil || src.Spec.Response.Unauthorized != nil { + dst.Spec.DenyWith = &v1beta1.DenyWith{} + } + + if denyWithSrc := src.Spec.Response.Unauthenticated; denyWithSrc != nil { + dst.Spec.DenyWith.Unauthenticated = convertDenyWithSpecTo(denyWithSrc) + } + + if denyWithSrc := src.Spec.Response.Unauthorized; denyWithSrc != nil { + dst.Spec.DenyWith.Unauthorized = convertDenyWithSpecTo(denyWithSrc) + } + } + + // callbacks + for name, callbackSrc := range src.Spec.Callbacks { + callback := convertCallbackTo(name, callbackSrc) + dst.Spec.Callbacks = append(dst.Spec.Callbacks, callback) + } + + // status + dst.Status = convertStatusTo(src.Status) + + logger.V(1).Info("finished converting resource", "dst", dst) + + return nil +} + +func (dst *AuthConfig) ConvertFrom(srcRaw conversion.Hub) error { + src := srcRaw.(*v1beta1.AuthConfig) + + logger := ctrl.Log.WithName("webhook").WithName("authconfig").WithName("converfrom").WithValues("src", src) + logger.V(1).Info("starting converting resource") + + // metadata + dst.ObjectMeta = src.ObjectMeta + + // hosts + dst.Spec.Hosts = src.Spec.Hosts + + // named patterns + if src.Spec.Patterns != nil { + dst.Spec.NamedPatterns = make(map[string]PatternExpressions, len(src.Spec.Patterns)) + for name, patterns := range src.Spec.Patterns { + dst.Spec.NamedPatterns[name] = utils.Map(patterns, convertPatternExpressionFrom) + } + } + + // conditions + dst.Spec.Conditions = utils.Map(src.Spec.Conditions, convertPatternExpressionOrRefFrom) + + // authentication + if src.Spec.Identity != nil { + dst.Spec.Authentication = make(map[string]AuthenticationSpec, len(src.Spec.Identity)) + for _, identity := range src.Spec.Identity { + name, authentication := convertAuthenticationFrom(identity) + dst.Spec.Authentication[name] = authentication + } + } + + // metadata + if src.Spec.Metadata != nil { + dst.Spec.Metadata = make(map[string]MetadataSpec, len(src.Spec.Metadata)) + for _, metadataSrc := range src.Spec.Metadata { + name, metadata := convertMetadataFrom(metadataSrc) + dst.Spec.Metadata[name] = metadata + } + } + + // authorization + if src.Spec.Authorization != nil { + dst.Spec.Authorization = make(map[string]AuthorizationSpec, len(src.Spec.Authorization)) + for _, authorizationSrc := range src.Spec.Authorization { + name, authorization := convertAuthorizationFrom(authorizationSrc) + dst.Spec.Authorization[name] = authorization + } + } + + // response + denyWith := src.Spec.DenyWith + + if denyWith != nil || len(src.Spec.Response) > 0 { + dst.Spec.Response = &ResponseSpec{} + } + + if denyWith != nil && denyWith.Unauthenticated != nil { + dst.Spec.Response.Unauthenticated = convertDenyWithSpecFrom(denyWith.Unauthenticated) + } + + if denyWith != nil && denyWith.Unauthorized != nil { + dst.Spec.Response.Unauthorized = convertDenyWithSpecFrom(denyWith.Unauthorized) + } + + for _, responseSrc := range src.Spec.Response { + if responseSrc.Wrapper != "httpHeader" && responseSrc.Wrapper != "" { + continue + } + if dst.Spec.Response.Success.Headers == nil { + dst.Spec.Response.Success.Headers = make(map[string]HeaderSuccessResponseSpec) + } + name, response := convertSuccessResponseFrom(responseSrc) + dst.Spec.Response.Success.Headers[name] = HeaderSuccessResponseSpec{ + SuccessResponseSpec: response, + } + } + + for _, responseSrc := range src.Spec.Response { + if responseSrc.Wrapper != "envoyDynamicMetadata" { + continue + } + if dst.Spec.Response.Success.DynamicMetadata == nil { + dst.Spec.Response.Success.DynamicMetadata = make(map[string]SuccessResponseSpec) + } + name, response := convertSuccessResponseFrom(responseSrc) + dst.Spec.Response.Success.DynamicMetadata[name] = response + } + + // callbacks + if src.Spec.Callbacks != nil { + dst.Spec.Callbacks = make(map[string]CallbackSpec, len(src.Spec.Callbacks)) + for _, callbackSrc := range src.Spec.Callbacks { + name, callback := convertCallbackFrom(callbackSrc) + dst.Spec.Callbacks[name] = callback + } + } + + // status + dst.Status = convertStatusFrom(src.Status) + + logger.V(1).Info("finished converting resource", "dst", dst) + + return nil +} + +func convertPatternExpressionTo(src PatternExpression) v1beta1.JSONPatternExpression { + return v1beta1.JSONPatternExpression{ + Selector: src.Selector, + Operator: v1beta1.JSONPatternOperator(src.Operator), + Value: src.Value, + } +} + +func convertPatternExpressionFrom(src v1beta1.JSONPatternExpression) PatternExpression { + return PatternExpression{ + Selector: src.Selector, + Operator: PatternExpressionOperator(src.Operator), + Value: src.Value, + } +} + +func convertPatternExpressionOrRefTo(src PatternExpressionOrRef) v1beta1.JSONPattern { + return v1beta1.JSONPattern{ + JSONPatternExpression: convertPatternExpressionTo(src.PatternExpression), + JSONPatternRef: v1beta1.JSONPatternRef{ + JSONPatternName: src.PatternRef.Name, + }, + } +} + +func convertPatternExpressionOrRefFrom(src v1beta1.JSONPattern) PatternExpressionOrRef { + return PatternExpressionOrRef{ + PatternExpression: convertPatternExpressionFrom(src.JSONPatternExpression), + PatternRef: PatternRef{ + Name: src.JSONPatternRef.JSONPatternName, + }, + } +} + +func convertEvaluatorCachingTo(src *EvaluatorCaching) *v1beta1.EvaluatorCaching { + if src == nil { + return nil + } + return &v1beta1.EvaluatorCaching{ + Key: convertValueOrSelectorTo(src.Key), + TTL: src.TTL, + } +} + +func convertEvaluatorCachingFrom(src *v1beta1.EvaluatorCaching) *EvaluatorCaching { + if src == nil { + return nil + } + return &EvaluatorCaching{ + Key: convertValueOrSelectorFrom(src.Key), + TTL: src.TTL, + } +} + +func convertValueOrSelectorTo(src ValueOrSelector) v1beta1.StaticOrDynamicValue { + return v1beta1.StaticOrDynamicValue{ + Value: gjson.ParseBytes(src.Value.Raw).String(), + ValueFrom: convertSelectorTo(src), + } +} + +func convertValueOrSelectorFrom(src v1beta1.StaticOrDynamicValue) ValueOrSelector { + value := k8sruntime.RawExtension{} + if src.ValueFrom.AuthJSON == "" { + value.Raw = []byte(fmt.Sprintf(`"%s"`, src.Value)) + } + return ValueOrSelector{ + Value: value, + Selector: src.ValueFrom.AuthJSON, + } +} + +func convertPtrValueOrSelectorTo(src *ValueOrSelector) *v1beta1.StaticOrDynamicValue { + if src == nil { + return nil + } + v := convertValueOrSelectorTo(*src) + return &v +} + +func convertPtrValueOrSelectorFrom(src *v1beta1.StaticOrDynamicValue) *ValueOrSelector { + if src == nil { + return nil + } + v := convertValueOrSelectorFrom(*src) + return &v +} + +func convertNamedValuesOrSelectorsTo(src NamedValuesOrSelectors) (jsonProperties []v1beta1.JsonProperty) { + if src == nil { + return nil + } + for name, valueOrSelector := range src { + jsonProperties = append(jsonProperties, v1beta1.JsonProperty{ + Name: name, + Value: valueOrSelector.Value, + ValueFrom: convertSelectorTo(valueOrSelector), + }) + } + return +} + +func convertNamedValuesOrSelectorsFrom(src []v1beta1.JsonProperty) NamedValuesOrSelectors { + if src == nil { + return nil + } + namedValuesOrSelectors := NamedValuesOrSelectors{} + for _, jsonProperty := range src { + namedValuesOrSelectors[jsonProperty.Name] = ValueOrSelector{ + Value: jsonProperty.Value, + Selector: jsonProperty.ValueFrom.AuthJSON, + } + } + return namedValuesOrSelectors +} + +func convertSelectorTo(src ValueOrSelector) v1beta1.ValueFrom { + return v1beta1.ValueFrom{ + AuthJSON: src.Selector, + } +} + +func convertCredentialsTo(src Credentials) v1beta1.Credentials { + var in, key string + switch src.GetType() { + case AuthorizationHeaderCredentials: + in = "authorization_header" + key = src.AuthorizationHeader.Prefix + case CustomHeaderCredentials: + in = "custom_header" + key = src.CustomHeader.Name + case QueryStringCredentials: + in = "query" + key = src.QueryString.Name + case CookieCredentials: + in = "cookie" + key = src.Cookie.Name + } + return v1beta1.Credentials{ + In: v1beta1.Credentials_In(in), + KeySelector: key, + } +} + +func convertCredentialsFrom(src v1beta1.Credentials) Credentials { + credentials := Credentials{} + switch src.In { + case "authorization_header": + credentials.AuthorizationHeader = &Prefixed{ + Prefix: src.KeySelector, + } + case "custom_header": + credentials.CustomHeader = &CustomHeader{ + Named: Named{Name: src.KeySelector}, + } + case "query": + credentials.QueryString = &Named{ + Name: src.KeySelector, + } + case "cookie": + credentials.Cookie = &Named{ + Name: src.KeySelector, + } + } + return credentials +} + +func convertAuthenticationTo(name string, src AuthenticationSpec) *v1beta1.Identity { + extendedProperties := utils.Map(convertNamedValuesOrSelectorsTo(NamedValuesOrSelectors(src.Overrides)), func(jsonProperty v1beta1.JsonProperty) v1beta1.ExtendedProperty { + return v1beta1.ExtendedProperty{ + JsonProperty: jsonProperty, + Overwrite: true, + } + }) + extendedProperties = append(extendedProperties, utils.Map(convertNamedValuesOrSelectorsTo(NamedValuesOrSelectors(src.Defaults)), func(jsonProperty v1beta1.JsonProperty) v1beta1.ExtendedProperty { + return v1beta1.ExtendedProperty{ + JsonProperty: jsonProperty, + Overwrite: false, + } + })...) + + identity := &v1beta1.Identity{ + Name: name, + Priority: src.Priority, + Metrics: src.Metrics, + Conditions: utils.Map(src.Conditions, convertPatternExpressionOrRefTo), + Cache: convertEvaluatorCachingTo(src.Cache), + Credentials: convertCredentialsTo(src.Credentials), + ExtendedProperties: extendedProperties, + } + + switch src.GetMethod() { + case ApiKeyAuthentication: + selector := *src.ApiKey.Selector + identity.APIKey = &v1beta1.Identity_APIKey{ + Selector: &selector, + AllNamespaces: src.ApiKey.AllNamespaces, + } + case JwtAuthentication: + identity.Oidc = &v1beta1.Identity_OidcConfig{ + Endpoint: src.Jwt.IssuerUrl, + TTL: src.Jwt.TTL, + } + case OAuth2TokenIntrospectionAuthentication: + credentials := *src.OAuth2TokenIntrospection.Credentials + identity.OAuth2 = &v1beta1.Identity_OAuth2Config{ + TokenIntrospectionUrl: src.OAuth2TokenIntrospection.Url, + TokenTypeHint: src.OAuth2TokenIntrospection.TokenTypeHint, + Credentials: &credentials, + } + case KubernetesTokenReviewAuthentication: + identity.KubernetesAuth = &v1beta1.Identity_KubernetesAuth{ + Audiences: src.KubernetesTokenReview.Audiences, + } + case X509ClientCertificateAuthentication: + selector := *src.X509ClientCertificate.Selector + identity.MTLS = &v1beta1.Identity_MTLS{ + Selector: &selector, + AllNamespaces: src.X509ClientCertificate.AllNamespaces, + } + case PlainIdentityAuthentication: + selector := v1beta1.Identity_Plain(v1beta1.ValueFrom{ + AuthJSON: src.Plain.Selector, + }) + identity.Plain = &selector + case AnonymousAccessAuthentication: + identity.Anonymous = &v1beta1.Identity_Anonymous{} + } + + return identity +} + +func convertAuthenticationFrom(src *v1beta1.Identity) (string, AuthenticationSpec) { + authentication := AuthenticationSpec{ + CommonEvaluatorSpec: CommonEvaluatorSpec{ + Priority: src.Priority, + Metrics: src.Metrics, + Conditions: utils.Map(src.Conditions, convertPatternExpressionOrRefFrom), + Cache: convertEvaluatorCachingFrom(src.Cache), + }, + Credentials: convertCredentialsFrom(src.Credentials), + } + + var overrides []v1beta1.JsonProperty + for _, extendedProperty := range src.ExtendedProperties { + if !extendedProperty.Overwrite { + continue + } + overrides = append(overrides, extendedProperty.JsonProperty) + } + if len(overrides) > 0 { + authentication.Overrides = ExtendedProperties(convertNamedValuesOrSelectorsFrom(overrides)) + } + + var defaults []v1beta1.JsonProperty + for _, extendedProperty := range src.ExtendedProperties { + if extendedProperty.Overwrite { + continue + } + defaults = append(defaults, extendedProperty.JsonProperty) + } + if len(defaults) > 0 { + authentication.Defaults = ExtendedProperties(convertNamedValuesOrSelectorsFrom(defaults)) + } + + switch src.GetType() { + case v1beta1.IdentityApiKey: + selector := *src.APIKey.Selector + authentication.ApiKey = &ApiKeyAuthenticationSpec{ + Selector: &selector, + AllNamespaces: src.APIKey.AllNamespaces, + } + case v1beta1.IdentityOidc: + authentication.Jwt = &JwtAuthenticationSpec{ + IssuerUrl: src.Oidc.Endpoint, + TTL: src.Oidc.TTL, + } + case v1beta1.IdentityOAuth2: + credentials := *src.OAuth2.Credentials + authentication.OAuth2TokenIntrospection = &OAuth2TokenIntrospectionSpec{ + Url: src.OAuth2.TokenIntrospectionUrl, + TokenTypeHint: src.OAuth2.TokenTypeHint, + Credentials: &credentials, + } + case v1beta1.IdentityKubernetesAuth: + authentication.KubernetesTokenReview = &KubernetesTokenReviewSpec{ + Audiences: src.KubernetesAuth.Audiences, + } + case v1beta1.IdentityMTLS: + selector := *src.MTLS.Selector + authentication.X509ClientCertificate = &X509ClientCertificateAuthenticationSpec{ + Selector: &selector, + AllNamespaces: src.MTLS.AllNamespaces, + } + case v1beta1.IdentityPlain: + authentication.Plain = &PlainIdentitySpec{ + Selector: src.Plain.AuthJSON, + } + case v1beta1.IdentityAnonymous: + authentication.AnonymousAccess = &AnonymousAccessSpec{} + } + + return src.Name, authentication +} + +func convertMetadataTo(name string, src MetadataSpec) *v1beta1.Metadata { + metadata := &v1beta1.Metadata{ + Name: name, + Priority: src.Priority, + Metrics: src.Metrics, + Conditions: utils.Map(src.Conditions, convertPatternExpressionOrRefTo), + Cache: convertEvaluatorCachingTo(src.Cache), + } + + switch src.GetMethod() { + case HttpMetadata: + metadata.GenericHTTP = convertHttpEndpointSpecTo(src.Http) + case UserInfoMetadata: + metadata.UserInfo = &v1beta1.Metadata_UserInfo{ + IdentitySource: src.UserInfo.IdentitySource, + } + case UmaResourceMetadata: + credentials := *src.Uma.Credentials + metadata.UMA = &v1beta1.Metadata_UMA{ + Endpoint: src.Uma.Endpoint, + Credentials: &credentials, + } + } + + return metadata +} + +func convertMetadataFrom(src *v1beta1.Metadata) (string, MetadataSpec) { + metadata := MetadataSpec{ + CommonEvaluatorSpec: CommonEvaluatorSpec{ + Priority: src.Priority, + Metrics: src.Metrics, + Conditions: utils.Map(src.Conditions, convertPatternExpressionOrRefFrom), + Cache: convertEvaluatorCachingFrom(src.Cache), + }, + } + + switch src.GetType() { + case v1beta1.MetadataGenericHTTP: + metadata.Http = convertHttpEndpointSpecFrom(src.GenericHTTP) + case v1beta1.MetadataUserinfo: + metadata.UserInfo = &UserInfoMetadataSpec{ + IdentitySource: src.UserInfo.IdentitySource, + } + case v1beta1.MetadataUma: + credentials := *src.UMA.Credentials + metadata.Uma = &UmaMetadataSpec{ + Endpoint: src.UMA.Endpoint, + Credentials: &credentials, + } + } + + return src.Name, metadata +} + +func convertHttpEndpointSpecTo(src *HttpEndpointSpec) *v1beta1.Metadata_GenericHTTP { + if src == nil { + return nil + } + return &v1beta1.Metadata_GenericHTTP{ + Endpoint: src.Url, + Method: convertMethodTo(src.Method), + Body: convertPtrValueOrSelectorTo(src.Body), + Parameters: convertNamedValuesOrSelectorsTo(src.Parameters), + ContentType: convertContentTypeTo(src.ContentType), + Headers: convertNamedValuesOrSelectorsTo(src.Headers), + SharedSecret: convertSecretKeyReferenceTo(src.SharedSecret), + OAuth2: convertOAuth2ClientAuthenticationTo(src.OAuth2), + Credentials: convertCredentialsTo(src.Credentials), + } +} + +func convertHttpEndpointSpecFrom(src *v1beta1.Metadata_GenericHTTP) *HttpEndpointSpec { + if src == nil { + return nil + } + return &HttpEndpointSpec{ + Url: src.Endpoint, + Method: convertMethodFrom(src.Method), + Body: convertPtrValueOrSelectorFrom(src.Body), + Parameters: convertNamedValuesOrSelectorsFrom(src.Parameters), + ContentType: convertContentTypeFrom(src.ContentType), + Headers: convertNamedValuesOrSelectorsFrom(src.Headers), + SharedSecret: convertSecretKeyReferenceFrom(src.SharedSecret), + OAuth2: convertOAuth2ClientAuthenticationFrom(src.OAuth2), + Credentials: convertCredentialsFrom(src.Credentials), + } +} + +func convertMethodTo(src *HttpMethod) *v1beta1.GenericHTTP_Method { + if src == nil { + return nil + } + method := v1beta1.GenericHTTP_Method(*src) + return &method +} + +func convertMethodFrom(src *v1beta1.GenericHTTP_Method) *HttpMethod { + if src == nil { + return nil + } + method := HttpMethod(*src) + return &method +} + +func convertContentTypeTo(src HttpContentType) v1beta1.Metadata_GenericHTTP_ContentType { + return v1beta1.Metadata_GenericHTTP_ContentType(src) +} + +func convertContentTypeFrom(src v1beta1.Metadata_GenericHTTP_ContentType) HttpContentType { + return HttpContentType(src) +} + +func convertOAuth2ClientAuthenticationTo(src *OAuth2ClientAuthentication) *v1beta1.OAuth2ClientAuthentication { + if src == nil { + return nil + } + o := &v1beta1.OAuth2ClientAuthentication{ + TokenUrl: src.TokenUrl, + ClientId: src.ClientId, + ClientSecret: *convertSecretKeyReferenceTo(&src.ClientSecret), + Scopes: src.Scopes, + ExtraParams: src.ExtraParams, + } + if src.Cache != nil { + cache := *src.Cache + o.Cache = &cache + } + return o +} + +func convertOAuth2ClientAuthenticationFrom(src *v1beta1.OAuth2ClientAuthentication) *OAuth2ClientAuthentication { + if src == nil { + return nil + } + o := &OAuth2ClientAuthentication{ + TokenUrl: src.TokenUrl, + ClientId: src.ClientId, + ClientSecret: *convertSecretKeyReferenceFrom(&src.ClientSecret), + Scopes: src.Scopes, + ExtraParams: src.ExtraParams, + } + if src.Cache != nil { + cache := *src.Cache + o.Cache = &cache + } + return o +} + +func convertSecretKeyReferenceTo(src *SecretKeyReference) *v1beta1.SecretKeyReference { + if src == nil { + return nil + } + return &v1beta1.SecretKeyReference{ + Name: src.Name, + Key: src.Key, + } +} + +func convertSecretKeyReferenceFrom(src *v1beta1.SecretKeyReference) *SecretKeyReference { + if src == nil { + return nil + } + return &SecretKeyReference{ + Name: src.Name, + Key: src.Key, + } +} + +func convertAuthorizationTo(name string, src AuthorizationSpec) *v1beta1.Authorization { + authorization := &v1beta1.Authorization{ + Name: name, + Priority: src.Priority, + Metrics: src.Metrics, + Conditions: utils.Map(src.Conditions, convertPatternExpressionOrRefTo), + Cache: convertEvaluatorCachingTo(src.Cache), + } + + switch src.GetMethod() { + case PatternMatchingAuthorization: + authorization.JSON = &v1beta1.Authorization_JSONPatternMatching{ + Rules: utils.Map(src.PatternMatching.Patterns, convertPatternExpressionOrRefTo), + } + case OpaAuthorization: + authorization.OPA = &v1beta1.Authorization_OPA{ + InlineRego: src.Opa.Rego, + ExternalRegistry: convertOpaExternalRegistryTo(src.Opa.External), + AllValues: src.Opa.AllValues, + } + case KubernetesSubjectAccessReviewAuthorization: + authorization.KubernetesAuthz = &v1beta1.Authorization_KubernetesAuthz{ + Groups: src.KubernetesSubjectAccessReview.Groups, + ResourceAttributes: convertKubernetesSubjectAccessReviewResourceAttributesTo(src.KubernetesSubjectAccessReview.ResourceAttributes), + } + if src.KubernetesSubjectAccessReview.User != nil { + authorization.KubernetesAuthz.User = convertValueOrSelectorTo(*src.KubernetesSubjectAccessReview.User) + } + case SpiceDBAuthorization: + authorization.Authzed = &v1beta1.Authorization_Authzed{ + Endpoint: src.SpiceDB.Endpoint, + Insecure: src.SpiceDB.Insecure, + SharedSecret: convertSecretKeyReferenceTo(src.SpiceDB.SharedSecret), + Subject: spiceDBObjectTo(src.SpiceDB.Subject), + Resource: spiceDBObjectTo(src.SpiceDB.Resource), + Permission: convertValueOrSelectorTo(src.SpiceDB.Permission), + } + } + + return authorization +} + +func convertAuthorizationFrom(src *v1beta1.Authorization) (string, AuthorizationSpec) { + authorization := AuthorizationSpec{ + CommonEvaluatorSpec: CommonEvaluatorSpec{ + Priority: src.Priority, + Metrics: src.Metrics, + Conditions: utils.Map(src.Conditions, convertPatternExpressionOrRefFrom), + Cache: convertEvaluatorCachingFrom(src.Cache), + }, + } + + switch src.GetType() { + case v1beta1.AuthorizationJSONPatternMatching: + authorization.PatternMatching = &PatternMatchingAuthorizationSpec{ + Patterns: utils.Map(src.JSON.Rules, convertPatternExpressionOrRefFrom), + } + case v1beta1.AuthorizationOPA: + authorization.Opa = &OpaAuthorizationSpec{ + Rego: src.OPA.InlineRego, + External: convertOpaExternalRegistryFrom(src.OPA.ExternalRegistry), + AllValues: src.OPA.AllValues, + } + case v1beta1.AuthorizationKubernetesAuthz: + authorization.KubernetesSubjectAccessReview = &KubernetesSubjectAccessReviewAuthorizationSpec{ + User: convertPtrValueOrSelectorFrom(&src.KubernetesAuthz.User), + Groups: src.KubernetesAuthz.Groups, + ResourceAttributes: convertKubernetesSubjectAccessReviewResourceAttributesFrom(src.KubernetesAuthz.ResourceAttributes), + } + case v1beta1.AuthorizationAuthzed: + authorization.SpiceDB = &SpiceDBAuthorizationSpec{ + Endpoint: src.Authzed.Endpoint, + Insecure: src.Authzed.Insecure, + SharedSecret: convertSecretKeyReferenceFrom(src.Authzed.SharedSecret), + Subject: spiceDBObjectFrom(src.Authzed.Subject), + Resource: spiceDBObjectFrom(src.Authzed.Resource), + Permission: convertValueOrSelectorFrom(src.Authzed.Permission), + } + } + + return src.Name, authorization +} + +func convertOpaExternalRegistryTo(src *ExternalOpaPolicy) v1beta1.ExternalRegistry { + if src == nil { + return v1beta1.ExternalRegistry{} + } + return v1beta1.ExternalRegistry{ + Endpoint: src.Url, + SharedSecret: convertSecretKeyReferenceTo(src.SharedSecret), + Credentials: convertCredentialsTo(src.Credentials), + TTL: src.TTL, + } +} + +func convertOpaExternalRegistryFrom(src v1beta1.ExternalRegistry) *ExternalOpaPolicy { + if src.Endpoint == "" { + return nil + } + return &ExternalOpaPolicy{ + HttpEndpointSpec: &HttpEndpointSpec{ + Url: src.Endpoint, + SharedSecret: convertSecretKeyReferenceFrom(src.SharedSecret), + Credentials: convertCredentialsFrom(src.Credentials), + }, + TTL: src.TTL, + } +} + +func convertKubernetesSubjectAccessReviewResourceAttributesTo(src *KubernetesSubjectAccessReviewResourceAttributesSpec) *v1beta1.Authorization_KubernetesAuthz_ResourceAttributes { + if src == nil { + return nil + } + return &v1beta1.Authorization_KubernetesAuthz_ResourceAttributes{ + Namespace: convertValueOrSelectorTo(src.Namespace), + Group: convertValueOrSelectorTo(src.Group), + Resource: convertValueOrSelectorTo(src.Resource), + Name: convertValueOrSelectorTo(src.Name), + SubResource: convertValueOrSelectorTo(src.SubResource), + Verb: convertValueOrSelectorTo(src.Verb), + } +} + +func convertKubernetesSubjectAccessReviewResourceAttributesFrom(src *v1beta1.Authorization_KubernetesAuthz_ResourceAttributes) *KubernetesSubjectAccessReviewResourceAttributesSpec { + if src == nil { + return nil + } + return &KubernetesSubjectAccessReviewResourceAttributesSpec{ + Namespace: convertValueOrSelectorFrom(src.Namespace), + Group: convertValueOrSelectorFrom(src.Group), + Resource: convertValueOrSelectorFrom(src.Resource), + Name: convertValueOrSelectorFrom(src.Name), + SubResource: convertValueOrSelectorFrom(src.SubResource), + Verb: convertValueOrSelectorFrom(src.Verb), + } +} + +func spiceDBObjectTo(src *SpiceDBObject) *v1beta1.AuthzedObject { + if src == nil { + return nil + } + return &v1beta1.AuthzedObject{ + Kind: convertValueOrSelectorTo(src.Kind), + Name: convertValueOrSelectorTo(src.Name), + } +} + +func spiceDBObjectFrom(src *v1beta1.AuthzedObject) *SpiceDBObject { + if src == nil { + return nil + } + return &SpiceDBObject{ + Kind: convertValueOrSelectorFrom(src.Kind), + Name: convertValueOrSelectorFrom(src.Name), + } +} + +func convertSuccessResponseTo(name string, src SuccessResponseSpec, wrapper string) *v1beta1.Response { + response := &v1beta1.Response{ + Name: name, + Priority: src.Priority, + Metrics: src.Metrics, + Conditions: utils.Map(src.Conditions, convertPatternExpressionOrRefTo), + Cache: convertEvaluatorCachingTo(src.Cache), + Wrapper: v1beta1.Response_Wrapper(wrapper), + WrapperKey: src.Key, + } + + switch src.GetMethod() { + case PlainAuthResponse: + selector := v1beta1.Response_Plain(convertValueOrSelectorTo(ValueOrSelector(*src.Plain))) + response.Plain = &selector + case JsonAuthResponse: + response.JSON = &v1beta1.Response_DynamicJSON{ + Properties: convertNamedValuesOrSelectorsTo(src.Json.Properties), + } + case WristbandAuthResponse: + response.Wristband = &v1beta1.Response_Wristband{ + Issuer: src.Wristband.Issuer, + CustomClaims: convertNamedValuesOrSelectorsTo(src.Wristband.CustomClaims), + } + if src.Wristband.TokenDuration != nil { + duration := *src.Wristband.TokenDuration + response.Wristband.TokenDuration = &duration + } + for _, keySrc := range src.Wristband.SigningKeyRefs { + if keySrc == nil { + continue + } + key := v1beta1.SigningKeyRef{ + Name: keySrc.Name, + Algorithm: v1beta1.SigningKeyAlgorithm(keySrc.Algorithm), + } + response.Wristband.SigningKeyRefs = append(response.Wristband.SigningKeyRefs, &key) + } + } + + return response +} + +func convertSuccessResponseFrom(src *v1beta1.Response) (string, SuccessResponseSpec) { + response := SuccessResponseSpec{ + CommonEvaluatorSpec: CommonEvaluatorSpec{ + Priority: src.Priority, + Metrics: src.Metrics, + Conditions: utils.Map(src.Conditions, convertPatternExpressionOrRefFrom), + Cache: convertEvaluatorCachingFrom(src.Cache), + }, + Key: src.WrapperKey, + } + + switch src.GetType() { + case v1beta1.ResponsePlain: + selector := PlainAuthResponseSpec(convertValueOrSelectorFrom(v1beta1.StaticOrDynamicValue(*src.Plain))) + response.Plain = &selector + case v1beta1.ResponseDynamicJSON: + response.Json = &JsonAuthResponseSpec{ + Properties: convertNamedValuesOrSelectorsFrom(src.JSON.Properties), + } + case v1beta1.ResponseWristband: + response.Wristband = &WristbandAuthResponseSpec{ + Issuer: src.Wristband.Issuer, + CustomClaims: convertNamedValuesOrSelectorsFrom(src.Wristband.CustomClaims), + } + if src.Wristband.TokenDuration != nil { + duration := *src.Wristband.TokenDuration + response.Wristband.TokenDuration = &duration + } + for _, keySrc := range src.Wristband.SigningKeyRefs { + if keySrc == nil { + continue + } + key := &WristbandSigningKeyRef{ + Name: keySrc.Name, + Algorithm: WristbandSigningKeyAlgorithm(keySrc.Algorithm), + } + response.Wristband.SigningKeyRefs = append(response.Wristband.SigningKeyRefs, key) + } + } + + return src.Name, response +} + +func convertDenyWithSpecTo(src *DenyWithSpec) *v1beta1.DenyWithSpec { + if src == nil { + return nil + } + return &v1beta1.DenyWithSpec{ + Code: v1beta1.DenyWith_Code(src.Code), + Headers: convertNamedValuesOrSelectorsTo(src.Headers), + Message: convertPtrValueOrSelectorTo(src.Message), + Body: convertPtrValueOrSelectorTo(src.Body), + } +} + +func convertDenyWithSpecFrom(src *v1beta1.DenyWithSpec) *DenyWithSpec { + if src == nil { + return nil + } + return &DenyWithSpec{ + Code: DenyWithCode(src.Code), + Headers: convertNamedValuesOrSelectorsFrom(src.Headers), + Message: convertPtrValueOrSelectorFrom(src.Message), + Body: convertPtrValueOrSelectorFrom(src.Body), + } +} + +func convertCallbackTo(name string, src CallbackSpec) *v1beta1.Callback { + callback := &v1beta1.Callback{ + Name: name, + Priority: src.Priority, + Metrics: src.Metrics, + Conditions: utils.Map(src.Conditions, convertPatternExpressionOrRefTo), + } + + switch src.GetMethod() { + case HttpCallback: + callback.HTTP = convertHttpEndpointSpecTo(src.Http) + } + + return callback +} + +func convertCallbackFrom(src *v1beta1.Callback) (string, CallbackSpec) { + callback := CallbackSpec{ + CommonEvaluatorSpec: CommonEvaluatorSpec{ + Priority: src.Priority, + Metrics: src.Metrics, + Conditions: utils.Map(src.Conditions, convertPatternExpressionOrRefFrom), + }, + } + + switch src.GetType() { + case v1beta1.CallbackHTTP: + callback.Http = convertHttpEndpointSpecFrom(src.HTTP) + } + + return src.Name, callback +} + +func convertStatusTo(src AuthConfigStatus) v1beta1.AuthConfigStatus { + return v1beta1.AuthConfigStatus{ + Conditions: utils.Map(src.Conditions, func(conditionSrc AuthConfigStatusCondition) v1beta1.Condition { + condition := v1beta1.Condition{ + Type: v1beta1.ConditionType(conditionSrc.Type), + Status: conditionSrc.Status, + LastTransitionTime: conditionSrc.LastTransitionTime, + Reason: conditionSrc.Reason, + Message: conditionSrc.Message, + } + if conditionSrc.LastUpdatedTime != nil { + time := *conditionSrc.LastUpdatedTime + condition.LastUpdatedTime = &time + } + return condition + }), + Summary: convertStatusSummaryTo(src.Summary), + } +} + +func convertStatusFrom(src v1beta1.AuthConfigStatus) AuthConfigStatus { + return AuthConfigStatus{ + Conditions: utils.Map(src.Conditions, func(conditionSrc v1beta1.Condition) AuthConfigStatusCondition { + condition := AuthConfigStatusCondition{ + Type: StatusConditionType(conditionSrc.Type), + Status: conditionSrc.Status, + LastTransitionTime: conditionSrc.LastTransitionTime, + Reason: conditionSrc.Reason, + Message: conditionSrc.Message, + } + if conditionSrc.LastUpdatedTime != nil { + time := *conditionSrc.LastUpdatedTime + condition.LastUpdatedTime = &time + } + return condition + }), + Summary: convertStatusSummaryFrom(src.Summary), + } +} + +func convertStatusSummaryTo(src AuthConfigStatusSummary) v1beta1.Summary { + hostsReady := make([]string, len(src.HostsReady)) + copy(hostsReady, src.HostsReady) + + return v1beta1.Summary{ + Ready: src.Ready, + HostsReady: hostsReady, + NumHostsReady: src.NumHostsReady, + NumIdentitySources: src.NumIdentitySources, + NumMetadataSources: src.NumMetadataSources, + NumAuthorizationPolicies: src.NumAuthorizationPolicies, + NumResponseItems: src.NumResponseItems, + FestivalWristbandEnabled: src.FestivalWristbandEnabled, + } +} + +func convertStatusSummaryFrom(src v1beta1.Summary) AuthConfigStatusSummary { + hostsReady := make([]string, len(src.HostsReady)) + copy(hostsReady, src.HostsReady) + + return AuthConfigStatusSummary{ + Ready: src.Ready, + HostsReady: hostsReady, + NumHostsReady: src.NumHostsReady, + NumIdentitySources: src.NumIdentitySources, + NumMetadataSources: src.NumMetadataSources, + NumAuthorizationPolicies: src.NumAuthorizationPolicies, + NumResponseItems: src.NumResponseItems, + FestivalWristbandEnabled: src.FestivalWristbandEnabled, + } +} diff --git a/api/v1beta2/auth_config_conversion_test.go b/api/v1beta2/auth_config_conversion_test.go new file mode 100644 index 00000000..0f6d95b1 --- /dev/null +++ b/api/v1beta2/auth_config_conversion_test.go @@ -0,0 +1,1003 @@ +package v1beta2 + +import ( + "encoding/json" + "reflect" + "sort" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/kuadrant/authorino/api/v1beta1" +) + +func TestConvertTo(t *testing.T) { + converted := &v1beta1.AuthConfig{} + authConfig().ConvertTo(converted) + + sort.Slice(converted.Spec.Identity, func(i, j int) bool { + return converted.Spec.Identity[i].Name < converted.Spec.Identity[j].Name + }) + sort.Slice(converted.Spec.Metadata, func(i, j int) bool { + return converted.Spec.Metadata[i].Name < converted.Spec.Metadata[j].Name + }) + sort.Slice(converted.Spec.Authorization, func(i, j int) bool { + return converted.Spec.Authorization[i].Name < converted.Spec.Authorization[j].Name + }) + sort.Slice(converted.Spec.Response, func(i, j int) bool { + return converted.Spec.Response[i].Name < converted.Spec.Response[j].Name + }) + for idx := range converted.Spec.Response { + if converted.Spec.Response[idx].Wristband != nil { + sort.Slice(converted.Spec.Response[idx].Wristband.CustomClaims, func(i, j int) bool { + return converted.Spec.Response[idx].Wristband.CustomClaims[i].Name < converted.Spec.Response[idx].Wristband.CustomClaims[j].Name + }) + } + if converted.Spec.Response[idx].JSON != nil { + sort.Slice(converted.Spec.Response[idx].JSON.Properties, func(i, j int) bool { + return converted.Spec.Response[idx].JSON.Properties[i].Name < converted.Spec.Response[idx].JSON.Properties[j].Name + }) + } + } + sort.Slice(converted.Spec.Callbacks, func(i, j int) bool { + return converted.Spec.Callbacks[i].Name < converted.Spec.Callbacks[j].Name + }) + + expected := hubAuthConfig() + if !reflect.DeepEqual(expected, converted) { + t.Error(cmp.Diff(expected, converted)) + } +} + +func TestConvertFrom(t *testing.T) { + converted := &AuthConfig{} + converted.ConvertFrom(hubAuthConfig()) + expected := authConfig() + if !reflect.DeepEqual(expected, converted) { + t.Error(cmp.Diff(expected, converted)) + } +} + +func authConfig() *AuthConfig { + authConfig := &AuthConfig{} + err := json.Unmarshal([]byte(` + { + "metadata": { + "name": "auth-config" + }, + "spec": { + "authentication": { + "anonymousAccess": { + "anonymous": {}, + "credentials": { + "authorizationHeader": {} + }, + "priority": 1 + }, + "apiKeyUsers": { + "apiKey": { + "selector": { + "matchLabels": { + "app": "talker-api", + "talker-api/credential-kind": "api-key" + } + } + }, + "credentials": { + "authorizationHeader": { + "prefix": "API-KEY" + } + }, + "overrides": { + "groups": { + "value": [ + "admin" + ] + } + } + }, + "fromEnvoy": { + "credentials": { + "authorizationHeader": {} + }, + "plain": { + "selector": "context.metadata_context.filter_metadata.envoy\\.filters\\.http\\.jwt_authn|verified_jwt" + }, + "when": [ + { + "operator": "neq", + "selector": "context.metadata_context.filter_metadata.envoy\\.filters\\.http\\.jwt_authn" + } + ] + }, + "k8sServiceAccountTokens": { + "credentials": { + "authorizationHeader": {} + }, + "kubernetesTokenReview": { + "audiences": [ + "talker-api.default.svc.cluster.local" + ] + } + }, + "mtlsUsers": { + "credentials": { + "authorizationHeader": {} + }, + "x509": { + "selector": { + "matchLabels": { + "app": "talker-api", + "talker-api/credential-kind": "ca-cert" + } + } + } + }, + "oauth2OpaqueTokens": { + "credentials": { + "authorizationHeader": {} + }, + "oauth2Introspection": { + "credentialsRef": { + "name": "oauth2-introspection-credentials" + }, + "endpoint": "https://accounts.company.com/oauth2/v1/introspect" + }, + "overrides": { + "jwtRBAC": { + "value": true + } + } + }, + "oidcServerUsers": { + "credentials": { + "authorizationHeader": {} + }, + "defaults": { + "username": { + "selector": "auth.identity.preferred_username" + } + }, + "jwt": { + "issuerUrl": "https://accounts.company.com", + "ttl": 3600 + }, + "overrides": { + "jwtRBAC": { + "value": true + } + } + } + }, + "authorization": { + "deny20percent": { + "opa": { + "rego": "allow { rand.intn(\"foo\", 100) < 80 }" + }, + "priority": 1 + }, + "externalOpaPolicy": { + "opa": { + "externalPolicy": { + "credentials": { + "authorizationHeader": {} + }, + "ttl": 3600, + "url": "https://raw.githubusercontent.com/repo/authorino-opa/main/allowed-methods.rego" + } + } + }, + "externalSpicedbPolicy": { + "spicedb": { + "endpoint": "spicedb.spicedb.svc.cluster.local:50051", + "insecure": true, + "permission": { + "selector": "context.request.http.method.@replace:{\"old\":\"GET\",\"new\":\"read\"}.@replace:{\"old\":\"POST\",\"new\":\"write\"}" + }, + "resource": { + "kind": { + "value": "blog/post" + }, + "name": { + "selector": "context.request.http.path.@extract:{\"sep\":\"/\",\"pos\":2}" + } + }, + "sharedSecretRef": { + "key": "grpc-preshared-key", + "name": "spicedb" + }, + "subject": { + "kind": { + "value": "blog/user" + }, + "name": { + "selector": "auth.identity.metadata.annotations.username" + } + } + } + }, + "inlineRego": { + "opa": { + "allValues": true, + "rego": "country = object.get(object.get(input.auth.metadata, \"geo-info\", {}), \"country_iso_code\", null)\nallow {\n allowed_countries := [\"ES\", \"FR\", \"IT\"]\n allowed_countries[_] == country\n}\n" + } + }, + "kubernetesRBAC": { + "kubernetesSubjectAccessReview": { + "user": { + "selector": "auth.identity.username" + } + }, + "when": [ + { + "patternRef": "admin-path" + }, + { + "operator": "eq", + "selector": "auth.identity.kubernetes-rbac", + "value": "true" + } + ] + }, + "simplePatternMatching": { + "patternMatching": { + "patterns": [ + { + "operator": "incl", + "selector": "auth.identity.roles", + "value": "admin" + } + ] + }, + "when": [ + { + "patternRef": "admin-path" + }, + { + "operator": "eq", + "selector": "auth.identity.jwtRBAC", + "value": "true" + } + ] + }, + "timestamp": { + "opa": { + "allValues": true, + "rego": "now = time.now_ns() / 1000000000\nallow = true\n" + }, + "priority": 20 + } + }, + "callbacks": { + "telemetry": { + "http": { + "body": { + "selector": "\\{\"requestId\":context.request.http.id,\"username\":\"{auth.identity.username}\",\"authorizationResult\":{auth.authorization}\\}\n" + }, + "contentType": "application/x-www-form-urlencoded", + "credentials": { + "authorizationHeader": {} + }, + "method": "POST", + "oauth2": { + "cache": true, + "clientId": "talker-api", + "clientSecretRef": { + "key": "client-secret", + "name": "talker-api-telemetry-credentials" + }, + "tokenUrl": "https://accounts.company.com/oauth2/v1/token" + }, + "url": "http://telemetry.server" + } + } + }, + "hosts": [ + "talker-api.127.0.0.1.nip.io", + "talker-api.default.svc.cluster.local" + ], + "metadata": { + "geoInfo": { + "cache": { + "key": { + "selector": "context.request.http.headers.x-forwarded-for.@extract:{\"sep\":\",\"}" + }, + "ttl": 3600 + }, + "http": { + "contentType": "application/x-www-form-urlencoded", + "credentials": { + "authorizationHeader": {} + }, + "headers": { + "Accept": { + "value": "application/json" + } + }, + "method": "GET", + "sharedSecretRef": { + "key": "shared-secret", + "name": "ip-location" + }, + "url": "http://ip-location.authorino.svc.cluster.local:3000/{context.request.http.headers.x-forwarded-for.@extract:{\"sep\":\",\"}}" + }, + "metrics": true + }, + "oidcUserInfo": { + "userInfo": { + "identitySource": "oidcServerUsers" + } + }, + "umaResourceInfo": { + "cache": { + "key": { + "selector": "context.request.http.path" + }, + "ttl": 60 + }, + "uma": { + "credentialsRef": { + "name": "talker-api-uma-credentials" + }, + "endpoint": "http://keycloak.authorino.svc.cluster.local:8080/auth/realms/kuadrant" + }, + "when": [ + { + "patternRef": "resourcePath" + } + ] + } + }, + "patterns": { + "adminPath": [ + { + "operator": "matches", + "selector": "context.request.http.path", + "value": "^/admin(/.*)?$" + } + ], + "resourcePath": [ + { + "operator": "matches", + "selector": "context.request.http.path", + "value": "^/greetings/\\d+$" + } + ] + }, + "response": { + "success": { + "dynamicMetadata": { + "username": { + "key": "", + "plain": { + "selector": "auth.identity.username" + } + } + }, + "headers": { + "festival-wristband": { + "key": "x-wristband-token", + "wristband": { + "customClaims": { + "scope": { + "selector": "context.request.http.method.@case:lower" + }, + "uri": { + "selector": "context.request.http.path" + }, + "username": { + "selector": "auth.identity.username" + } + }, + "issuer": "https://authorino-authorino-oidc.authorino.svc.cluster.local:8083/authorino/e2e-test/festival-wristband", + "signingKeyRefs": [ + { + "algorithm": "ES256", + "name": "wristband-signing-key" + } + ], + "tokenDuration": 300 + } + }, + "x-auth-data": { + "json": { + "properties": { + "geo": { + "selector": "auth.metadata.geoInfo" + }, + "timestamp": { + "selector": "auth.authorization.timestamp" + }, + "username": { + "selector": "auth.identity.username" + } + } + }, + "key": "" + }, + "x-auth-service": { + "key": "", + "plain": { + "value": "Authorino" + } + } + } + }, + "unauthenticated": { + "message": { + "value": "Authentication failed" + } + }, + "unauthorized": { + "headers": { + "random": { + "selector": "auth.authorization.deny20percent" + } + }, + "message": { + "value": "Access denied" + } + } + }, + "when": [ + { + "operator": "neq", + "selector": "context.metadata_context.filter_metadata.envoy\\.filters\\.http\\.skipper_lua_filter|skip", + "value": "true" + } + ] + }, + "status": { + "summary": { + "ready": false, + "hostsReady": [], + "numHostsReady": "", + "numIdentitySources": 0, + "numMetadataSources": 0, + "numAuthorizationPolicies": 0, + "numResponseItems": 0, + "festivalWristbandEnabled": false + } + } + }`), &authConfig) + if err != nil { + panic(err) + } + return authConfig +} + +func hubAuthConfig() *v1beta1.AuthConfig { + authConfig := &v1beta1.AuthConfig{} + err := json.Unmarshal([]byte(` + { + "metadata": { + "name": "auth-config" + }, + "spec": { + "authorization": [ + { + "metrics": false, + "name": "deny20percent", + "opa": { + "allValues": false, + "inlineRego": "allow { rand.intn(\"foo\", 100) < 80 }" + }, + "priority": 1 + }, + { + "metrics": false, + "name": "externalOpaPolicy", + "opa": { + "allValues": false, + "externalRegistry": { + "credentials": { + "in": "authorization_header", + "keySelector": "" + }, + "endpoint": "https://raw.githubusercontent.com/repo/authorino-opa/main/allowed-methods.rego", + "ttl": 3600 + } + }, + "priority": 0 + }, + { + "authzed": { + "endpoint": "spicedb.spicedb.svc.cluster.local:50051", + "insecure": true, + "permission": { + "valueFrom": { + "authJSON": "context.request.http.method.@replace:{\"old\":\"GET\",\"new\":\"read\"}.@replace:{\"old\":\"POST\",\"new\":\"write\"}" + } + }, + "resource": { + "kind": { + "value": "blog/post", + "valueFrom": {} + }, + "name": { + "valueFrom": { + "authJSON": "context.request.http.path.@extract:{\"sep\":\"/\",\"pos\":2}" + } + } + }, + "sharedSecretRef": { + "key": "grpc-preshared-key", + "name": "spicedb" + }, + "subject": { + "kind": { + "value": "blog/user", + "valueFrom": {} + }, + "name": { + "valueFrom": { + "authJSON": "auth.identity.metadata.annotations.username" + } + } + } + }, + "metrics": false, + "name": "externalSpicedbPolicy", + "priority": 0 + }, + { + "metrics": false, + "name": "inlineRego", + "opa": { + "allValues": true, + "inlineRego": "country = object.get(object.get(input.auth.metadata, \"geo-info\", {}), \"country_iso_code\", null)\nallow {\n allowed_countries := [\"ES\", \"FR\", \"IT\"]\n allowed_countries[_] == country\n}\n" + }, + "priority": 0 + }, + { + "kubernetes": { + "user": { + "valueFrom": { + "authJSON": "auth.identity.username" + } + } + }, + "metrics": false, + "name": "kubernetesRBAC", + "priority": 0, + "when": [ + { + "patternRef": "admin-path" + }, + { + "operator": "eq", + "selector": "auth.identity.kubernetes-rbac", + "value": "true" + } + ] + }, + { + "json": { + "rules": [ + { + "operator": "incl", + "selector": "auth.identity.roles", + "value": "admin" + } + ] + }, + "metrics": false, + "name": "simplePatternMatching", + "priority": 0, + "when": [ + { + "patternRef": "admin-path" + }, + { + "operator": "eq", + "selector": "auth.identity.jwtRBAC", + "value": "true" + } + ] + }, + { + "metrics": false, + "name": "timestamp", + "opa": { + "allValues": true, + "inlineRego": "now = time.now_ns() / 1000000000\nallow = true\n" + }, + "priority": 20 + } + ], + "callbacks": [ + { + "http": { + "body": { + "valueFrom": { + "authJSON": "\\{\"requestId\":context.request.http.id,\"username\":\"{auth.identity.username}\",\"authorizationResult\":{auth.authorization}\\}\n" + } + }, + "contentType": "application/x-www-form-urlencoded", + "credentials": { + "in": "authorization_header", + "keySelector": "" + }, + "endpoint": "http://telemetry.server", + "method": "POST", + "oauth2": { + "cache": true, + "clientId": "talker-api", + "clientSecretRef": { + "key": "client-secret", + "name": "talker-api-telemetry-credentials" + }, + "tokenUrl": "https://accounts.company.com/oauth2/v1/token" + } + }, + "metrics": false, + "name": "telemetry", + "priority": 0 + } + ], + "denyWith": { + "unauthenticated": { + "message": { + "value": "Authentication failed", + "valueFrom": {} + } + }, + "unauthorized": { + "headers": [ + { + "name": "random", + "valueFrom": { + "authJSON": "auth.authorization.deny20percent" + } + } + ], + "message": { + "value": "Access denied", + "valueFrom": {} + } + } + }, + "hosts": [ + "talker-api.127.0.0.1.nip.io", + "talker-api.default.svc.cluster.local" + ], + "identity": [ + { + "anonymous": {}, + "credentials": { + "in": "authorization_header", + "keySelector": "" + }, + "metrics": false, + "name": "anonymousAccess", + "priority": 1 + }, + { + "apiKey": { + "allNamespaces": false, + "selector": { + "matchLabels": { + "app": "talker-api", + "talker-api/credential-kind": "api-key" + } + } + }, + "credentials": { + "in": "authorization_header", + "keySelector": "API-KEY" + }, + "extendedProperties": [ + { + "name": "groups", + "overwrite": true, + "value": [ + "admin" + ], + "valueFrom": {} + } + ], + "metrics": false, + "name": "apiKeyUsers", + "priority": 0 + }, + { + "credentials": { + "in": "authorization_header", + "keySelector": "" + }, + "metrics": false, + "name": "fromEnvoy", + "plain": { + "authJSON": "context.metadata_context.filter_metadata.envoy\\.filters\\.http\\.jwt_authn|verified_jwt" + }, + "priority": 0, + "when": [ + { + "operator": "neq", + "selector": "context.metadata_context.filter_metadata.envoy\\.filters\\.http\\.jwt_authn" + } + ] + }, + { + "credentials": { + "in": "authorization_header", + "keySelector": "" + }, + "kubernetes": { + "audiences": [ + "talker-api.default.svc.cluster.local" + ] + }, + "metrics": false, + "name": "k8sServiceAccountTokens", + "priority": 0 + }, + { + "credentials": { + "in": "authorization_header", + "keySelector": "" + }, + "metrics": false, + "mtls": { + "allNamespaces": false, + "selector": { + "matchLabels": { + "app": "talker-api", + "talker-api/credential-kind": "ca-cert" + } + } + }, + "name": "mtlsUsers", + "priority": 0 + }, + { + "credentials": { + "in": "authorization_header", + "keySelector": "" + }, + "extendedProperties": [ + { + "name": "jwtRBAC", + "overwrite": true, + "value": true, + "valueFrom": {} + } + ], + "metrics": false, + "name": "oauth2OpaqueTokens", + "oauth2": { + "credentialsRef": { + "name": "oauth2-introspection-credentials" + }, + "tokenIntrospectionUrl": "https://accounts.company.com/oauth2/v1/introspect" + }, + "priority": 0 + }, + { + "credentials": { + "in": "authorization_header", + "keySelector": "" + }, + "extendedProperties": [ + { + "name": "jwtRBAC", + "overwrite": true, + "value": true, + "valueFrom": {} + }, + { + "name": "username", + "overwrite": false, + "valueFrom": { + "authJSON": "auth.identity.preferred_username" + } + } + ], + "metrics": false, + "name": "oidcServerUsers", + "oidc": { + "endpoint": "https://accounts.company.com", + "ttl": 3600 + }, + "priority": 0 + } + ], + "metadata": [ + { + "cache": { + "key": { + "valueFrom": { + "authJSON": "context.request.http.headers.x-forwarded-for.@extract:{\"sep\":\",\"}" + } + }, + "ttl": 3600 + }, + "http": { + "contentType": "application/x-www-form-urlencoded", + "credentials": { + "in": "authorization_header", + "keySelector": "" + }, + "endpoint": "http://ip-location.authorino.svc.cluster.local:3000/{context.request.http.headers.x-forwarded-for.@extract:{\"sep\":\",\"}}", + "headers": [ + { + "name": "Accept", + "value": "application/json", + "valueFrom": {} + } + ], + "method": "GET", + "sharedSecretRef": { + "key": "shared-secret", + "name": "ip-location" + } + }, + "metrics": true, + "name": "geoInfo", + "priority": 0 + }, + { + "metrics": false, + "name": "oidcUserInfo", + "priority": 0, + "userInfo": { + "identitySource": "oidcServerUsers" + } + }, + { + "cache": { + "key": { + "valueFrom": { + "authJSON": "context.request.http.path" + } + }, + "ttl": 60 + }, + "metrics": false, + "name": "umaResourceInfo", + "priority": 0, + "uma": { + "credentialsRef": { + "name": "talker-api-uma-credentials" + }, + "endpoint": "http://keycloak.authorino.svc.cluster.local:8080/auth/realms/kuadrant" + }, + "when": [ + { + "patternRef": "resourcePath" + } + ] + } + ], + "patterns": { + "adminPath": [ + { + "operator": "matches", + "selector": "context.request.http.path", + "value": "^/admin(/.*)?$" + } + ], + "resourcePath": [ + { + "operator": "matches", + "selector": "context.request.http.path", + "value": "^/greetings/\\d+$" + } + ] + }, + "response": [ + { + "metrics": false, + "name": "festival-wristband", + "priority": 0, + "wrapper": "httpHeader", + "wrapperKey": "x-wristband-token", + "wristband": { + "customClaims": [ + { + "name": "scope", + "valueFrom": { + "authJSON": "context.request.http.method.@case:lower" + } + }, + { + "name": "uri", + "valueFrom": { + "authJSON": "context.request.http.path" + } + }, + { + "name": "username", + "valueFrom": { + "authJSON": "auth.identity.username" + } + } + ], + "issuer": "https://authorino-authorino-oidc.authorino.svc.cluster.local:8083/authorino/e2e-test/festival-wristband", + "signingKeyRefs": [ + { + "algorithm": "ES256", + "name": "wristband-signing-key" + } + ], + "tokenDuration": 300 + } + }, + { + "metrics": false, + "name": "username", + "plain": { + "valueFrom": { + "authJSON": "auth.identity.username" + } + }, + "priority": 0, + "wrapper": "envoyDynamicMetadata", + "wrapperKey": "" + }, + { + "json": { + "properties": [ + { + "name": "geo", + "valueFrom": { + "authJSON": "auth.metadata.geoInfo" + } + }, + { + "name": "timestamp", + "valueFrom": { + "authJSON": "auth.authorization.timestamp" + } + }, + { + "name": "username", + "valueFrom": { + "authJSON": "auth.identity.username" + } + } + ] + }, + "metrics": false, + "name": "x-auth-data", + "priority": 0, + "wrapper": "httpHeader", + "wrapperKey": "" + }, + { + "metrics": false, + "name": "x-auth-service", + "plain": { + "value": "Authorino", + "valueFrom": {} + }, + "priority": 0, + "wrapper": "httpHeader", + "wrapperKey": "" + } + ], + "when": [ + { + "operator": "neq", + "selector": "context.metadata_context.filter_metadata.envoy\\.filters\\.http\\.skipper_lua_filter|skip", + "value": "true" + } + ] + }, + "status": { + "summary": { + "ready": false, + "hostsReady": [], + "numHostsReady": "", + "numIdentitySources": 0, + "numMetadataSources": 0, + "numAuthorizationPolicies": 0, + "numResponseItems": 0, + "festivalWristbandEnabled": false + } + } + }`), &authConfig) + if err != nil { + panic(err) + } + return authConfig +} diff --git a/api/v1beta2/auth_config_types.go b/api/v1beta2/auth_config_types.go new file mode 100644 index 00000000..dbec9df3 --- /dev/null +++ b/api/v1beta2/auth_config_types.go @@ -0,0 +1,872 @@ +/* +Copyright 2020 Red Hat, Inc. + +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 v1beta2 + +import ( + k8score "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + k8sruntime "k8s.io/apimachinery/pkg/runtime" +) + +const ( + // The following constants are used to identify the different methods of authentication. + UnknownAuthenticationMethod AuthenticationMethod = iota + ApiKeyAuthentication + JwtAuthentication + OAuth2TokenIntrospectionAuthentication + KubernetesTokenReviewAuthentication + X509ClientCertificateAuthentication + PlainIdentityAuthentication + AnonymousAccessAuthentication + + // The following constants are used to identify the different methods of metadata fetching. + UnknownMetadataMethod MetadataMethod = iota + HttpMetadata + UserInfoMetadata + UmaResourceMetadata + + // The following constants are used to identify the different methods of authorization. + UnknownAuthorizationMethod AuthorizationMethod = iota + PatternMatchingAuthorization + OpaAuthorization + KubernetesSubjectAccessReviewAuthorization + SpiceDBAuthorization + + // The following constants are used to identify the different methods of auth response. + UnknownAuthResponseMethod AuthResponseMethod = iota + PlainAuthResponse + JsonAuthResponse + WristbandAuthResponse + + // The following constants are used to identify the different methods of callback functions. + UnknownCallbackMethod CallbackMethod = iota + HttpCallback + + // The following constants are used to identify the different types of credentials. + UnknownCredentialsType CredentialsType = iota + AuthorizationHeaderCredentials + CustomHeaderCredentials + QueryStringCredentials + CookieCredentials + + // Status conditions + StatusConditionAvailable StatusConditionType = "Available" + StatusConditionReady StatusConditionType = "Ready" + + // Status reasons + StatusReasonReconciling string = "Reconciling" + StatusReasonReconciled string = "Reconciled" + StatusReasonInvalidResource string = "Invalid" + StatusReasonHostsLinked string = "HostsLinked" + StatusReasonHostsNotLinked string = "HostsNotLinked" + StatusReasonCachingError string = "CachingError" + StatusReasonUnknown string = "Unknown" + + EvaluatorDefaultCacheTTL = 60 +) + +type AuthenticationMethod int8 +type MetadataMethod int8 +type AuthorizationMethod int8 +type AuthResponseMethod int8 +type CallbackMethod int8 +type CredentialsType int8 + +type StatusConditionType string + +// AuthConfig is the schema for Authorino's AuthConfig API +// +kubebuilder:object:root=true +// +kubebuilder:subresource:status +// +kubebuilder:printcolumn:name="Ready",type=string,JSONPath=`.status.summary.ready`,description="Ready for all hosts" +// +kubebuilder:printcolumn:name="Hosts",type=string,JSONPath=`.status.summary.numHostsReady`,description="Number of hosts ready" +// +kubebuilder:printcolumn:name="Authentication",type=integer,JSONPath=`.status.summary.numIdentitySources`,description="Number of trusted identity sources",priority=2 +// +kubebuilder:printcolumn:name="Metadata",type=integer,JSONPath=`.status.summary.numMetadataSources`,description="Number of external metadata sources",priority=2 +// +kubebuilder:printcolumn:name="Authorization",type=integer,JSONPath=`.status.summary.numAuthorizationPolicies`,description="Number of authorization policies",priority=2 +// +kubebuilder:printcolumn:name="Response",type=integer,JSONPath=`.status.summary.numResponseItems`,description="Number of items added to the authorization response",priority=2 +// +kubebuilder:printcolumn:name="Wristband",type=boolean,JSONPath=`.status.summary.festivalWristbandEnabled`,description="Whether issuing Festival Wristbands",priority=2 +type AuthConfig struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec AuthConfigSpec `json:"spec,omitempty"` + Status AuthConfigStatus `json:"status,omitempty"` +} + +// Specifies the desired state of the AuthConfig resource, i.e. the authencation/authorization scheme to be applied to protect the matching service hosts. +type AuthConfigSpec struct { + // The list of public host names of the services protected by this authentication/authorization scheme. + // Authorino uses the requested host to lookup for the corresponding authentication/authorization configs to enforce. + Hosts []string `json:"hosts"` + + // Named sets of patterns that can be referred in `when` conditions and in pattern-matching authorization policy rules. + // +optional + NamedPatterns map[string]PatternExpressions `json:"patterns,omitempty"` + + // Overall conditions for the AuthConfig to be enforced. + // If omitted, the AuthConfig will be enforced at all requests. + // If present, all conditions must match for the AuthConfig to be enforced; otherwise, Authorino skips the AuthConfig and returns to the auth request with status OK. + // +optional + Conditions []PatternExpressionOrRef `json:"when,omitempty"` + + // Authentication configs. + // At least one config MUST evaluate to a valid identity object for the auth request to be successful. + // +optional + Authentication map[string]AuthenticationSpec `json:"authentication,omitempty"` + + // Metadata sources. + // Authorino fetches auth metadata as JSON from sources specified in this config. + // +optional + Metadata map[string]MetadataSpec `json:"metadata,omitempty"` + + // Authorization policies. + // All policies MUST evaluate to "allowed = true" for the auth request be successful. + // +optional + Authorization map[string]AuthorizationSpec `json:"authorization,omitempty"` + + // Response items. + // Authorino builds custom responses to the client of the auth request. + // +optional + Response *ResponseSpec `json:"response,omitempty"` + + // Callback functions. + // Authorino sends callbacks at the end of the auth pipeline to the endpoints specified in this config. + // +optional + Callbacks map[string]CallbackSpec `json:"callbacks,omitempty"` +} + +type PatternExpressions []PatternExpression + +type PatternExpression struct { + // Path selector to fetch content from the authorization JSON (e.g. 'request.method'). + // Any pattern supported by https://pkg.go.dev/github.com/tidwall/gjson can be used. + // Authorino custom JSON path modifiers are also supported. + Selector string `json:"selector,omitempty"` + // The binary operator to be applied to the content fetched from the authorization JSON, for comparison with "value". + // Possible values are: "eq" (equal to), "neq" (not equal to), "incl" (includes; for arrays), "excl" (excludes; for arrays), "matches" (regex) + Operator PatternExpressionOperator `json:"operator,omitempty"` + // The value of reference for the comparison with the content fetched from the authorization JSON. + // If used with the "matches" operator, the value must compile to a valid Golang regex. + Value string `json:"value,omitempty"` +} + +// +kubebuilder:validation:Enum:=eq;neq;incl;excl;matches +type PatternExpressionOperator string + +type PatternExpressionOrRef struct { + PatternExpression `json:",omitempty"` + PatternRef `json:",omitempty"` +} + +type PatternRef struct { + // Reference to a named set of pattern expressions + Name string `json:"patternRef,omitempty"` +} + +type NamedValuesOrSelectors map[string]ValueOrSelector + +type ValueOrSelector struct { + // Static value + // +kubebuilder:validation:Schemaless + // +kubebuilder:pruning:PreserveUnknownFields + Value k8sruntime.RawExtension `json:"value,omitempty"` + + // Simple path selector to fetch content from the authorization JSON (e.g. 'request.method') or a string template with variables that resolve to patterns (e.g. "Hello, {auth.identity.name}!"). + // Any pattern supported by https://pkg.go.dev/github.com/tidwall/gjson can be used. + // The following Authorino custom modifiers are supported: @extract:{sep:" ",pos:0}, @replace{old:"",new:""}, @case:upper|lower, @base64:encode|decode and @strip. + Selector string `json:"selector,omitempty"` +} + +type CommonEvaluatorSpec struct { + // Priority group of the config. + // All configs in the same priority group are evaluated concurrently; consecutive priority groups are evaluated sequentially. + // +optional + // +kubebuilder:default:=0 + Priority int `json:"priority,omitempty"` + + // Whether this config should generate individual observability metrics + // +optional + // +kubebuilder:default:=false + Metrics bool `json:"metrics,omitempty"` + + // Conditions for Authorino to enforce this config. + // If omitted, the config will be enforced for all requests. + // If present, all conditions must match for the config to be enforced; otherwise, the config will be skipped. + // +optional + Conditions []PatternExpressionOrRef `json:"when,omitempty"` + + // Caching options for the resolved object returned when applying this config. + // Omit it to avoid caching objects for this config. + // +optional + Cache *EvaluatorCaching `json:"cache,omitempty"` +} + +type EvaluatorCaching struct { + // Key used to store the entry in the cache. + // The resolved key must be unique within the scope of this particular config. + Key ValueOrSelector `json:"key"` + + // Duration (in seconds) of the external data in the cache before pulled again from the source. + // +optional + // +kubebuilder:default:=60 + TTL int `json:"ttl,omitempty"` +} + +type AuthenticationSpec struct { + CommonEvaluatorSpec `json:",omitempty"` + + // Defines where credentials are required to be passed in the request for authentication based on this config. + // If omitted, it defaults to credentials passed in the HTTP Authorization header and the "Bearer" prefix prepended to the secret credential value. + // +optional + Credentials Credentials `json:"credentials,omitempty"` + + // Overrides the resolved identity object by setting the additional properties (claims) specified in this config, + // before appending the object to the authorization JSON. + // It requires the resolved identity object to always be a JSON object. + // Do not use this option with identity objects of other JSON types (array, string, etc). + // +optional + Overrides ExtendedProperties `json:"overrides,omitempty"` + + // Set default property values (claims) for the resolved identity object, that are set before appending the object to + // the authorization JSON. If the property is already present in the resolved identity object, the default value is ignored. + // It requires the resolved identity object to always be a JSON object. + // Do not use this option with identity objects of other JSON types (array, string, etc). + // +optional + Defaults ExtendedProperties `json:"defaults,omitempty"` + + AuthenticationMethodSpec `json:""` +} + +func (s *AuthenticationSpec) GetMethod() AuthenticationMethod { + if s.ApiKey != nil { + return ApiKeyAuthentication + } else if s.Jwt != nil { + return JwtAuthentication + } else if s.OAuth2TokenIntrospection != nil { + return OAuth2TokenIntrospectionAuthentication + } else if s.X509ClientCertificate != nil { + return X509ClientCertificateAuthentication + } else if s.KubernetesTokenReview != nil { + return KubernetesTokenReviewAuthentication + } else if s.Plain != nil { + return PlainIdentityAuthentication + } else if s.AnonymousAccess != nil { + return AnonymousAccessAuthentication + } + return UnknownAuthenticationMethod +} + +type Credentials struct { + AuthorizationHeader *Prefixed `json:"authorizationHeader,omitempty"` + CustomHeader *CustomHeader `json:"customHeader,omitempty"` + QueryString *Named `json:"queryString,omitempty"` + Cookie *Named `json:"cookie,omitempty"` +} + +func (c *Credentials) GetType() CredentialsType { + if c.AuthorizationHeader != nil { + return AuthorizationHeaderCredentials + } else if c.CustomHeader != nil { + return CustomHeaderCredentials + } else if c.QueryString != nil { + return QueryStringCredentials + } else if c.Cookie != nil { + return CookieCredentials + } + return UnknownCredentialsType +} + +type Named struct { + Name string `json:"name"` +} + +type Prefixed struct { + Prefix string `json:"prefix,omitempty"` +} + +type CustomHeader struct { + Named `json:""` +} + +type ExtendedProperties NamedValuesOrSelectors + +type AuthenticationMethodSpec struct { + // Authentication based on API keys stored in Kubernetes secrets. + ApiKey *ApiKeyAuthenticationSpec `json:"apiKey,omitempty"` + // Authentication based on JWT tokens. + Jwt *JwtAuthenticationSpec `json:"jwt,omitempty"` + // Authentication by OAuth2 token introspection. + OAuth2TokenIntrospection *OAuth2TokenIntrospectionSpec `json:"oauth2Introspection,omitempty"` + // Authentication by Kubernetes token review. + KubernetesTokenReview *KubernetesTokenReviewSpec `json:"kubernetesTokenReview,omitempty"` + // Authentication based on client X.509 certificates. + // The certificates presented by the clients must be signed by a trusted CA whose certificates are stored in Kubernetes secrets. + X509ClientCertificate *X509ClientCertificateAuthenticationSpec `json:"x509,omitempty"` + // Identity object extracted from the context. + // Use this method when authentication is performed beforehand by a proxy and the resulting object passed to Authorino as JSON in the auth request. + Plain *PlainIdentitySpec `json:"plain,omitempty"` + // Anonymous access. + AnonymousAccess *AnonymousAccessSpec `json:"anonymous,omitempty"` +} + +// Settings to select the API key Kubernetes secrets. +type ApiKeyAuthenticationSpec struct { + // Label selector used by Authorino to match secrets from the cluster storing valid credentials to authenticate to this service + Selector *metav1.LabelSelector `json:"selector"` + + // Whether Authorino should look for API key secrets in all namespaces or only in the same namespace as the AuthConfig. + // Enabling this option in namespaced Authorino instances has no effect. + // +optional + // +kubebuilder:default:=false + AllNamespaces bool `json:"allNamespaces,omitempty"` +} + +// Settings to fetch the JSON Web Key Set (JWKS) for the JWT authentication. +type JwtAuthenticationSpec struct { + // URL of the issuer of the JWT. + // If `jwksUrl` is omitted, Authorino will append the path to the OpenID Connect Well-Known Discovery endpoint + // (i.e. "/.well-known/openid-configuration") to this URL, to discover the OIDC configuration where to obtain + // the "jkws_uri" claim from. + // The value must coincide with the value of the "iss" (issuer) claim of the discovered OpenID Connect configuration. + // +optional + IssuerUrl string `json:"issuerUrl"` + + // Decides how long to wait before refreshing the JWKS (in seconds). + // If omitted, Authorino will never refresh the JWKS. + // +optional + TTL int `json:"ttl,omitempty"` +} + +// Settings to perform the OAuth2 token introspection request. +type OAuth2TokenIntrospectionSpec struct { + // The full URL of the token introspection endpoint. + Url string `json:"endpoint"` + + // The token type hint for the token introspection. + // If omitted, it defaults to "access_token". + // +optional + TokenTypeHint string `json:"tokenTypeHint,omitempty"` + + // Reference to a Kubernetes secret in the same namespace, that stores client credentials to the OAuth2 server. + Credentials *k8score.LocalObjectReference `json:"credentialsRef"` +} + +// Parameters of the Kubernetes TokenReview request +type KubernetesTokenReviewSpec struct { + // The list of audiences (scopes) that must be claimed in a Kubernetes authentication token supplied in the request, and reviewed by Authorino. + // If omitted, Authorino will review tokens expecting the host name of the requested protected service amongst the audiences. + // +optional + Audiences []string `json:"audiences,omitempty"` +} + +// Settings to authenticate clients by X.509 certificates. +type X509ClientCertificateAuthenticationSpec struct { + // Label selector used by Authorino to match secrets from the cluster storing trusted CA certificates to validate + // clients trying to authenticate to this service + Selector *metav1.LabelSelector `json:"selector"` + + // Whether Authorino should look for TLS secrets in all namespaces or only in the same namespace as the AuthConfig. + // Enabling this option in namespaced Authorino instances has no effect. + // +optional + // +kubebuilder:default:=false + AllNamespaces bool `json:"allNamespaces,omitempty"` +} + +// Settings to extract the identity object from the context. +type PlainIdentitySpec struct { + // Simple path selector to fetch content from the authorization JSON (e.g. 'request.method') or a string template with variables that resolve to patterns (e.g. "Hello, {auth.identity.name}!"). + // Any pattern supported by https://pkg.go.dev/github.com/tidwall/gjson can be used. + // The following Authorino custom modifiers are supported: @extract:{sep:" ",pos:0}, @replace{old:"",new:""}, @case:upper|lower, @base64:encode|decode and @strip. + Selector string `json:"selector"` +} + +type AnonymousAccessSpec struct{} + +type MetadataSpec struct { + CommonEvaluatorSpec `json:""` + MetadataMethodSpec `json:""` +} + +func (s *MetadataSpec) GetMethod() MetadataMethod { + if s.Http != nil { + return HttpMetadata + } else if s.UserInfo != nil { + return UserInfoMetadata + } else if s.Uma != nil { + return UmaResourceMetadata + } + return UnknownMetadataMethod +} + +type MetadataMethodSpec struct { + // External source of auth metadata via HTTP request + Http *HttpEndpointSpec `json:"http,omitempty"` + // OpendID Connect UserInfo linked to an OIDC authentication config specified in this same AuthConfig. + UserInfo *UserInfoMetadataSpec `json:"userInfo,omitempty"` + // User-Managed Access (UMA) source of resource data. + Uma *UmaMetadataSpec `json:"uma,omitempty"` +} + +// Settings of the external HTTP request +type HttpEndpointSpec struct { + // Endpoint URL of the HTTP service. + // The value can include variable placeholders in the format "{selector}", where "selector" is any pattern supported + // by https://pkg.go.dev/github.com/tidwall/gjson and selects value from the authorization JSON. + // E.g. https://ext-auth-server.io/metadata?p={request.path} + Url string `json:"url"` + + // HTTP verb used in the request to the service. Accepted values: GET (default), POST. + // When the request method is POST, the authorization JSON is passed in the body of the request. + // +optional + // +kubebuilder:default:=GET + Method *HttpMethod `json:"method,omitempty"` + + // Raw body of the HTTP request. + // Supersedes 'bodyParameters'; use either one or the other. + // Use it with method=POST; for GET requests, set parameters as query string in the 'endpoint' (placeholders can be used). + // +optional + Body *ValueOrSelector `json:"body,omitempty"` + + // Custom parameters to encode in the body of the HTTP request. + // Superseded by 'body'; use either one or the other. + // Use it with method=POST; for GET requests, set parameters as query string in the 'endpoint' (placeholders can be used). + // +optional + Parameters NamedValuesOrSelectors `json:"bodyParameters,omitempty"` + + // Content-Type of the request body. Shapes how 'bodyParameters' are encoded. + // Use it with method=POST; for GET requests, Content-Type is automatically set to 'text/plain'. + // +optional + // +kubebuilder:default:=application/x-www-form-urlencoded + ContentType HttpContentType `json:"contentType,omitempty"` + + // Custom headers in the HTTP request. + // +optional + Headers NamedValuesOrSelectors `json:"headers,omitempty"` + + // Reference to a Secret key whose value will be passed by Authorino in the request. + // The HTTP service can use the shared secret to authenticate the origin of the request. + // Ignored if used together with oauth2. + // +optional + SharedSecret *SecretKeyReference `json:"sharedSecretRef,omitempty"` + + // Authentication with the HTTP service by OAuth2 Client Credentials grant. + // +optional + OAuth2 *OAuth2ClientAuthentication `json:"oauth2,omitempty"` + + // Defines where client credentials will be passed in the request to the service. + // If omitted, it defaults to client credentials passed in the HTTP Authorization header and the "Bearer" prefix expected prepended to the secret value. + // +optional + Credentials Credentials `json:"credentials,omitempty"` +} + +// +kubebuilder:validation:Enum:=GET;POST;PUT;PATCH;DELETE;HEAD;OPTIONS;CONNECT;TRACE +type HttpMethod string + +// +kubebuilder:validation:Enum:=application/x-www-form-urlencoded;application/json +type HttpContentType string + +// Reference to a Kubernetes secret +type SecretKeyReference struct { + // The name of the secret in the Authorino's namespace to select from. + Name string `json:"name"` + + // The key of the secret to select from. Must be a valid secret key. + Key string `json:"key"` +} + +// Settings for OAuth2 client authentication with the external service +type OAuth2ClientAuthentication struct { + // Token endpoint URL of the OAuth2 resource server. + TokenUrl string `json:"tokenUrl"` + // OAuth2 Client ID. + ClientId string `json:"clientId"` + // Reference to a Kuberentes Secret key that stores that OAuth2 Client Secret. + ClientSecret SecretKeyReference `json:"clientSecretRef"` + // Optional scopes for the client credentials grant, if supported by he OAuth2 server. + Scopes []string `json:"scopes,omitempty"` + // Optional extra parameters for the requests to the token URL. + ExtraParams map[string]string `json:"extraParams,omitempty"` + // Caches and reuses the token until expired. + // Set it to false to force fetch the token at every authorization request regardless of expiration. + // +kubebuilder:default:=true + Cache *bool `json:"cache,omitempty"` +} + +// Settings of the OpendID Connect UserInfo linked to an OIDC-enabled JWT authentication config of this same AuthConfig. +type UserInfoMetadataSpec struct { + // The name of an OIDC-enabled JWT authentication config whose OpenID Connect configuration discovered includes the OIDC "userinfo_endpoint" claim. + IdentitySource string `json:"identitySource"` +} + +// Settings of the User-Managed Access (UMA) source of resource data. +type UmaMetadataSpec struct { + // The endpoint of the UMA server. + // The value must coincide with the "issuer" claim of the UMA config discovered from the well-known uma configuration endpoint. + Endpoint string `json:"endpoint"` + + // Reference to a Kubernetes secret in the same namespace, that stores client credentials to the resource registration API of the UMA server. + Credentials *k8score.LocalObjectReference `json:"credentialsRef"` +} + +type AuthorizationSpec struct { + CommonEvaluatorSpec `json:""` + AuthorizationMethodSpec `json:""` +} + +func (s *AuthorizationSpec) GetMethod() AuthorizationMethod { + if s.PatternMatching != nil { + return PatternMatchingAuthorization + } else if s.Opa != nil { + return OpaAuthorization + } else if s.KubernetesSubjectAccessReview != nil { + return KubernetesSubjectAccessReviewAuthorization + } else if s.SpiceDB != nil { + return SpiceDBAuthorization + } + return UnknownAuthorizationMethod +} + +type AuthorizationMethodSpec struct { + // Pattern-matching authorization rules. + PatternMatching *PatternMatchingAuthorizationSpec `json:"patternMatching,omitempty"` + // Open Policy Agent (OPA) Rego policy. + Opa *OpaAuthorizationSpec `json:"opa,omitempty"` + // Authorization by Kubernetes SubjectAccessReview + KubernetesSubjectAccessReview *KubernetesSubjectAccessReviewAuthorizationSpec `json:"kubernetesSubjectAccessReview,omitempty"` + // Authorization decision delegated to external Authzed/SpiceDB server. + SpiceDB *SpiceDBAuthorizationSpec `json:"spicedb,omitempty"` +} + +type PatternMatchingAuthorizationSpec struct { + Patterns []PatternExpressionOrRef `json:"patterns"` +} + +// Settings of the Open Policy Agent (OPA) authorization. +type OpaAuthorizationSpec struct { + // Authorization policy as a Rego language document. + // The Rego document must include the "allow" condition, set by Authorino to "false" by default (i.e. requests are unauthorized unless changed). + // The Rego document must NOT include the "package" declaration in line 1. + Rego string `json:"rego,omitempty"` + + // Settings for fetching the OPA policy from an external registry. + // Use it alternatively to 'rego'. + // For the configurations of the HTTP request, the following options are not implemented: 'method', 'body', 'bodyParameters', + // 'contentType', 'headers', 'oauth2'. Use it only with: 'url', 'sharedSecret', 'credentials'. + External *ExternalOpaPolicy `json:"externalPolicy,omitempty"` + + // Returns the value of all Rego rules in the virtual document. Values can be read in subsequent evaluators/phases of the Auth Pipeline. + // Otherwise, only the default `allow` rule will be exposed. + // Returning all Rego rules can affect performance of OPA policies during reconciliation (policy precompile) and at runtime. + // +kubebuilder:default:=false + AllValues bool `json:"allValues,omitempty"` +} + +// ExternalOpaPolicy sets the configs for fetching OPA policies from an external source. +type ExternalOpaPolicy struct { + *HttpEndpointSpec `json:""` + + // Duration (in seconds) of the external data in the cache before pulled again from the source. + TTL int `json:"ttl,omitempty"` +} + +// Parameters of the Kubernetes SubjectAccessReview request. +type KubernetesSubjectAccessReviewAuthorizationSpec struct { + // User to check for authorization in the Kubernetes RBAC. + // Omit it to check for group authorization only. + User *ValueOrSelector `json:"user,omitempty"` + + // Groups the user must be a member of or, if `user` is omitted, the groups to check for authorization in the Kubernetes RBAC. + Groups []string `json:"groups,omitempty"` + + // Use resourceAttributes to check permissions on Kubernetes resources. + // If omitted, it performs a non-resource SubjectAccessReview, with verb and path inferred from the request. + // +optional + ResourceAttributes *KubernetesSubjectAccessReviewResourceAttributesSpec `json:"resourceAttributes,omitempty"` +} + +type KubernetesSubjectAccessReviewResourceAttributesSpec struct { + // API group of the resource. + // Use '*' for all API groups. + Group ValueOrSelector `json:"group,omitempty"` + // Resource kind + // Use '*' for all resource kinds. + Resource ValueOrSelector `json:"resource,omitempty"` + // Subresource kind + SubResource ValueOrSelector `json:"subresource,omitempty"` + // Resource name + // Omit it to check for authorization on all resources of the specified kind. + Name ValueOrSelector `json:"name,omitempty"` + // Namespace where the user must have permissions on the resource. + Namespace ValueOrSelector `json:"namespace,omitempty"` + // Verb to check for authorization on the resource. + // Use '*' for all verbs. + Verb ValueOrSelector `json:"verb,omitempty"` +} + +// Settings of the check request to the external SpiceDB server. +type SpiceDBAuthorizationSpec struct { + // Hostname and port number to the GRPC interface of the SpiceDB server (e.g. spicedb:50051). + Endpoint string `json:"endpoint"` + + // Insecure HTTP connection (i.e. disables TLS verification) + Insecure bool `json:"insecure,omitempty"` + + // Reference to a Secret key whose value will be used by Authorino to authenticate with the Authzed service. + SharedSecret *SecretKeyReference `json:"sharedSecretRef,omitempty"` + + // The subject that will be checked for the permission or relation. + Subject *SpiceDBObject `json:"subject,omitempty"` + + // The resource on which to check the permission or relation. + Resource *SpiceDBObject `json:"resource,omitempty"` + + // The name of the permission (or relation) on which to execute the check. + Permission ValueOrSelector `json:"permission,omitempty"` +} + +type SpiceDBObject struct { + Name ValueOrSelector `json:"name,omitempty"` + Kind ValueOrSelector `json:"kind,omitempty"` +} + +// Settings of the custom auth response. +type ResponseSpec struct { + // Customizations on the denial status attributes when the request is unauthenticated. + // For integration of Authorino via proxy, the proxy must honour the response status attributes specified in this config. + // Default: 401 Unauthorized + // +optional + Unauthenticated *DenyWithSpec `json:"unauthenticated,omitempty"` + + // Customizations on the denial status attributes when the request is unauthorized. + // For integration of Authorino via proxy, the proxy must honour the response status attributes specified in this config. + // Default: 403 Forbidden + // +optional + Unauthorized *DenyWithSpec `json:"unauthorized,omitempty"` + + // Response items to be included in the auth response when the request is authenticated and authorized. + // For integration of Authorino via proxy, the proxy must use these settings to propagate dynamic metadata and/or inject data in the request. + // +optional + Success WrappedSuccessResponseSpec `json:"success,omitempty"` +} + +// +kubebuilder:validation:Minimum:=300 +// +kubebuilder:validation:Maximum:=599 +type DenyWithCode int64 + +// Setting of the custom denial response. +type DenyWithSpec struct { + // HTTP status code to override the default denial status code. + Code DenyWithCode `json:"code,omitempty"` + + // HTTP message to override the default denial message. + Message *ValueOrSelector `json:"message,omitempty"` + + // HTTP response headers to override the default denial headers. + Headers NamedValuesOrSelectors `json:"headers,omitempty"` + + // HTTP response body to override the default denial body. + Body *ValueOrSelector `json:"body,omitempty"` +} + +// Settings of the custom success response. +type WrappedSuccessResponseSpec struct { + // Custom success response items wrapped as HTTP headers. + // For integration of Authorino via proxy, the proxy must use these settings to inject data in the request. + Headers map[string]HeaderSuccessResponseSpec `json:"headers,omitempty"` + + // Custom success response items wrapped as HTTP headers. + // For integration of Authorino via proxy, the proxy must use these settings to propagate dynamic metadata. + // See https://www.envoyproxy.io/docs/envoy/latest/configuration/advanced/well_known_dynamic_metadata + DynamicMetadata map[string]SuccessResponseSpec `json:"dynamicMetadata,omitempty"` +} + +type HeaderSuccessResponseSpec struct { + SuccessResponseSpec `json:",omitempty"` +} + +// Settings of the success custom response item. +type SuccessResponseSpec struct { + CommonEvaluatorSpec `json:""` + AuthResponseMethodSpec `json:""` + + // The key used to add the custom response item (name of the HTTP header or root property of the Dynamic Metadata object). + // If omitted, it will be set to the name of the response config. + Key string `json:"key,omitempty"` +} + +func (s *SuccessResponseSpec) GetMethod() AuthResponseMethod { + if s.Plain != nil { + return PlainAuthResponse + } else if s.Json != nil { + return JsonAuthResponse + } else if s.Wristband != nil { + return WristbandAuthResponse + } + return UnknownAuthResponseMethod +} + +// Settings of the custom success response item. +type AuthResponseMethodSpec struct { + // Plain text content + Plain *PlainAuthResponseSpec `json:"plain,omitempty"` + // JSON object + // Specify it as the list of properties of the object, whose values can combine static values and values selected from the authorization JSON. + Json *JsonAuthResponseSpec `json:"json,omitempty"` + // Authorino Festival Wristband token + Wristband *WristbandAuthResponseSpec `json:"wristband,omitempty"` +} + +// Static value or selector to set the plain custom response item. +type PlainAuthResponseSpec ValueOrSelector + +// List of properties of the JSON object to set the custom response item +// The values can be static or selected from the authorization JSON. +type JsonAuthResponseSpec struct { + Properties NamedValuesOrSelectors `json:"properties"` +} + +// Settings of the Festival Wristband token custom response item. +type WristbandAuthResponseSpec struct { + // The endpoint to the Authorino service that issues the wristband (format: ://:/, where = /.plain`)](#plain-text-responsesuccessheadersdynamicmetadataplain) + - [JSON injection (`response.success..json`)](#json-injection-responsesuccessheadersdynamicmetadatajson) + - [Festival Wristband tokens (`response.success..wristband`)](#festival-wristband-tokens-responsesuccessheadersdynamicmetadatawristband) - [Callbacks (`callbacks`)](#callbacks-callbacks) - [HTTP endpoints (`callbacks.http`)](#http-endpoints-callbackshttp) - [Common feature: Priorities](#common-feature-priorities) @@ -45,21 +43,21 @@ ## Overview -We call _features_ of Authorino the different things one can do to enforce identity verification & authentication and authorization on requests against protected services. These can be a specific identity verification method based on a supported authentication protocol, or a method to fetch additional auth metadata in request-time, etc. +We call _features_ of Authorino the different things one can do to enforce identity verification & authentication and authorization on requests to protected services. These can be a specific identity verification method based on a supported authentication protocol, or a method to fetch additional auth metadata in request-time, etc. -Most features of Authorino relate to the different phases of the [Auth Pipeline](./architecture.md#the-auth-pipeline-aka-enforcing-protection-in-request-time) and therefore are configured in the Authorino [`AuthConfig`](./architecture.md#the-authorino-authconfig-custom-resource-definition-crd). An _identity verification feature_ usually refers to a functionality of Authorino such as the [API key-based authentication](#api-key-identityapikey) implemented by Authorino, the [validation of JWTs/OIDC ID tokens](#openid-connect-oidc-jwtjose-verification-and-validation-identityoidc), and authentication based on [Kubernetes TokenReviews](#kubernetes-tokenreview-identitykubernetes). Analogously, [OPA](#open-policy-agent-opa-rego-policies-authorizationopa), [JSON pattern-matching](#json-pattern-matching-authorization-rules-authorizationjson) and [Kubernetes SubjectAccessReview](#kubernetes-subjectaccessreview-authorizationkubernetes) are examples of _authorization features_ of Authorino. +Most features of Authorino relate to the different phases of the [Auth Pipeline](./architecture.md#the-auth-pipeline-aka-enforcing-protection-in-request-time) and therefore are configured in the Authorino [`AuthConfig`](./architecture.md#the-authorino-authconfig-custom-resource-definition-crd). An _identity verification/authentication feature_ usually refers to a functionality of Authorino such as the [API key-based authentication](#api-key-authenticationapikey), the [validation of JWTs/OIDC ID tokens](#jwt-verification-authenticationjwt), and authentication based on [Kubernetes TokenReviews](#kubernetes-tokenreview-authenticationkubernetestokenreview). Analogously, [OPA](#open-policy-agent-opa-rego-policies-authorizationopa), [pattern-matching](#pattern-matching-authorization-authorizationpatternmatching) and [Kubernetes SubjectAccessReview](#kubernetes-subjectaccessreview-authorizationkubernetessubjectaccessreview) are examples of _authorization features_ of Authorino. -At a deeper level, a _feature_ can also be an additional functionality within a bigger feature, usually applicable to the whole class the bigger feature belongs to. For instance, the configuration of the location and key selector of [auth credentials](#extra-auth-credentials-credentials), available for all identity verification-related features. Other examples would be [_Identity extension_](#extra-identity-extension-extendedproperties) and [_Response wrappers_](#extra-response-wrappers-wrapper-and-wrapperkey). +At a deeper level, a _feature_ can also be an additional functionality within a bigger feature, usually applicable to the whole class the bigger feature belongs to. For instance, the configuration of how [auth credentials](#extra-auth-credentials-authenticationcredentials) expected to be carried in the request, which is broadly available for any identity verification method. Other examples are: [_Identity extension_](#extra-identity-extension-authenticationdefaults-and-authenticationoverrides) and [Priorities](#common-feature-priorities). A full specification of all features of Authorino that can be configured in an `AuthConfig` can be found in the official [spec](../install/crd/authorino.kuadrant.io_authconfigs.yaml) of the custom resource definition. -You can also learn about Authorino features by using the [`kubectl explain`](https://kubernetes.io/docs/reference/generated/kubectl/kubectl-commands#explain) command in a Kubernetes cluster where the Authorino CRD has been installed. E.g. `kubectl explain authconfigs.spec.identity.extendedProperties`. +You can also learn about Authorino features by using the [`kubectl explain`](https://kubernetes.io/docs/reference/generated/kubectl/kubectl-commands#explain) command in a Kubernetes cluster where the Authorino CRD has been installed. E.g. `kubectl explain authconfigs.spec.authentication.credentials`. -## Common feature: JSON paths ([`valueFrom.authJSON`](https://pkg.go.dev/github.com/kuadrant/authorino/api/v1beta1?utm_source=gopls#ValueFromAuthJSON)) +## Common feature: JSON paths ([`selector`](https://pkg.go.dev/github.com/kuadrant/authorino/api/v1beta2?utm_source=gopls#ValueOrSelector)) -The first feature of Authorino to learn about is a common functionality, used in the specification of many other features. _JSON paths_ have to do with reading data from the [Authorization JSON](./architecture.md#the-authorization-json), to refer to them in configuration of dynamic steps of API protection enforcing. +The first feature of Authorino to learn about is a common functionality used in the specification of many other features. _JSON paths_ are selectors of data from the [Authorization JSON](./architecture.md#the-authorization-json) used in parts of an AuthConfig for referring to dynamic values of each authorization request. -Usage examples of JSON paths are: dynamic URL and request parameters when fetching metadata from external sources, dynamic authorization policy rules, and dynamic authorization responses (injected JSON and Festival Wristband token claims). +Usage examples of JSON paths are: dynamic URLs and request parameters when fetching metadata from external sources, dynamic authorization policy rules, and dynamic authorization response attributes (e.g. injected HTTP headers, Festival Wristband token claims, etc). ### Syntax @@ -115,33 +113,33 @@ In combination with `@extract`, `@base64` can be used to extract the username in _JSON paths_ can be interpolated into strings to build template-like dynamic values. E.g. `"Hello, {auth.identity.name}!"`. -## Identity verification & authentication features ([`identity`](https://pkg.go.dev/github.com/kuadrant/authorino/api/v1beta1?utm_source=gopls#Identity)) +## Identity verification & authentication features ([`authentication`](https://pkg.go.dev/github.com/kuadrant/authorino/api/v1beta2?utm_source=gopls#AuthenticationSpec)) -### API key ([`identity.apiKey`](https://pkg.go.dev/github.com/kuadrant/authorino/api/v1beta1?utm_source=gopls#Identity_APIKey)) +### API key ([`authentication.apiKey`](https://pkg.go.dev/github.com/kuadrant/authorino/api/v1beta2?utm_source=gopls#ApiKeyAuthenticationSpec)) Authorino relies on Kubernetes `Secret` resources to represent API keys. To define an API key, create a `Secret` in the cluster containing an `api_key` entry that holds the value of the API key. -API key secrets must be created in the same namespace of the `AuthConfig` (default) or `spec.identity.apiKey.allNamespaces` must be set to `true` (only works with [cluster-wide Authorino instances](./architecture.md#cluster-wide-vs-namespaced-instances)). +API key secrets must be created in the same namespace of the `AuthConfig` (default) or `spec.authentication.apiKey.allNamespaces` must be set to `true` (only works with [cluster-wide Authorino instances](./architecture.md#cluster-wide-vs-namespaced-instances)). -API key secrets must be labeled with the labels that match the selectors specified in `spec.identity.apiKey.selector` in the `AuthConfig`. +API key secrets must be labeled with the labels that match the selectors specified in `spec.authentication.apiKey.selector` in the `AuthConfig`. -Whenever an `AuthConfig` is indexed, Authorino will also index all matching API key secrets. In order for Authorino to also watch events related to API key secrets individually (e.g. new `Secret` created, updates, deletion/revocation), `Secret`s must also include a label that matches Authorino's bootstrap configuration `--secret-label-selector` (default: `authorino.kuadrant.io/managed-by=authorino`). This label may or may not be present to `spec.identity.apiKey.selector` in the `AuthConfig` without implications for the caching of the API keys when triggered by the reconciliation of the `AuthConfig`; however, if not present, individual changes related to the API key secret (i.e. without touching the `AuthConfig`) will be ignored by the reconciler. +Whenever an `AuthConfig` is indexed, Authorino will also index all matching API key secrets. In order for Authorino to also watch events related to API key secrets individually (e.g. new `Secret` created, updates, deletion/revocation), `Secret`s must also include a label that matches Authorino's bootstrap configuration `--secret-label-selector` (default: `authorino.kuadrant.io/managed-by=authorino`). This label may or may not be present to `spec.authentication.apiKey.selector` in the `AuthConfig` without implications for the caching of the API keys when triggered by the reconciliation of the `AuthConfig`; however, if not present, individual changes related to the API key secret (i.e. without touching the `AuthConfig`) will be ignored by the reconciler. **Example.** For the following `AuthConfig`: ```yaml -apiVersion: authorino.kuadrant.io/v1beta1 +apiVersion: authorino.kuadrant.io/v1beta2 kind: AuthConfig metadata: name: my-api-protection namespace: authorino-system spec: hosts: - - my-api.io - identity: - - name: api-key-users + - my-api.io + authentication: + "api-key-users": apiKey: selector: matchLabels: # the key-value set used to select the matching `Secret`s; resources including these labels will be accepted as valid API keys to authenticate to this service @@ -167,7 +165,7 @@ type: Opaque The resolved identity object, added to the authorization JSON following an API key identity source evaluation, is the Kubernetes `Secret` resource (as JSON). -### Kubernetes TokenReview ([`identity.kubernetes`](https://pkg.go.dev/github.com/kuadrant/authorino/api/v1beta1?utm_source=gopls#Identity_KubernetesAuth)) +### Kubernetes TokenReview ([`authentication.kubernetesTokenReview`](https://pkg.go.dev/github.com/kuadrant/authorino/api/v1beta2?utm_source=gopls#KubernetesTokenReviewSpec)) Authorino can verify Kubernetes-valid access tokens (using Kubernetes [TokenReview](https://kubernetes.io/docs/reference/kubernetes-api/authentication-resources/token-review-v1) API). @@ -178,39 +176,39 @@ The list of `audiences` of the token must include the requested host and port of For the following `AuthConfig` CR, the Kubernetes token must include the audience `my-api.io`: ```yaml -apiVersion: authorino.kuadrant.io/v1beta1 +apiVersion: authorino.kuadrant.io/v1beta2 kind: AuthConfig metadata: name: my-api-protection spec: hosts: - - my-api.io - identity: - - name: cluster-users - kubernetes: {} + - my-api.io + authentication: + "cluster-users": + kubernetesTokenReview: {} ``` Whereas for the following `AuthConfig` CR, the Kubernetes token audiences must include **foo** and **bar**: ```yaml -apiVersion: authorino.kuadrant.io/v1beta1 +apiVersion: authorino.kuadrant.io/v1beta2 kind: AuthConfig metadata: name: my-api-protection spec: hosts: - - my-api.io - identity: - - name: cluster-users - kubernetes: + - my-api.io + authentication: + "cluster-users": + kubernetesTokenReview: audiences: - - foo - - bar + - foo + - bar ``` The resolved identity object added to the authorization JSON following a successful Kubernetes authentication identity evaluation is the `status` field of TokenReview response (see [TokenReviewStatus](https://kubernetes.io/docs/reference/kubernetes-api/authentication-resources/token-review-v1/#TokenReviewStatus) for reference). -### OpenID Connect (OIDC) JWT/JOSE verification and validation ([`identity.oidc`](https://pkg.go.dev/github.com/kuadrant/authorino/api/v1beta1?utm_source=gopls#Identity_OidcConfig)) +### JWT verification ([`authentication.jwt`](https://pkg.go.dev/github.com/kuadrant/authorino/api/v1beta2?utm_source=gopls#JwtAuthenticationSpec)) In reconciliation-time, using [OpenID Connect Discovery well-known endpoint](https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderConfig), Authorino automatically discovers and caches OpenID Connect configurations and associated JSON Web Key Sets (JWKS) for all OpenID Connect issuers declared in an `AuthConfig`. Then, in request-time, Authorino verifies the JSON Web Signature (JWS) and check the time validity of signed JSON Web Tokens (JWT) supplied on each request. @@ -222,11 +220,11 @@ The `kid` claim stated in the JWT header must match one of the keys cached by Au The decoded payload of the validated JWT is appended to the authorization JSON as the resolved identity. -OpenID Connect configurations and linked JSON Web Key Sets can be configured to be automatically refreshed (pull again from the OpenID Connect Discovery well-known endpoints), by setting the `identity.oidc.ttl` field (given in seconds, default: `0` – i.e. auto-refresh disabled). +OpenID Connect configurations and linked JSON Web Key Sets can be configured to be automatically refreshed (pull again from the OpenID Connect Discovery well-known endpoints), by setting the `authentication.jwt.ttl` field (given in seconds, default: `0` – i.e. auto-refresh disabled). For an excellent summary of the underlying concepts and standards that relate OpenID Connect and JSON Object Signing and Encryption (JOSE), see this [article](https://access.redhat.com/blogs/766093/posts/1976593) by Jan Rusnacko. For official specification and RFCs, see [OpenID Connect Core](https://openid.net/specs/openid-connect-core-1_0.html), [OpenID Connect Discovery](https://openid.net/specs/openid-connect-discovery-1_0.html), [JSON Web Token (JWT) (RFC7519)](https://datatracker.ietf.org/doc/html/rfc7519), and [JSON Object Signing and Encryption (JOSE)](http://www.iana.org/assignments/jose/jose.xhtml). -### OAuth 2.0 introspection ([`identity.oauth2`](https://pkg.go.dev/github.com/kuadrant/authorino/api/v1beta1?utm_source=gopls#Identity_OAuth2Config)) +### OAuth 2.0 introspection ([`authentication.oauth2Introspection`](https://pkg.go.dev/github.com/kuadrant/authorino/api/v1beta2?utm_source=gopls#OAuth2TokenIntrospectionSpec)) For bare OAuth 2.0 implementations, Authorino can perform token introspection on the access tokens supplied in the requests to protected APIs. @@ -238,23 +236,13 @@ Developers must set the token introspection endpoint in the `AuthConfig`, as wel The response returned by the OAuth2 server to the token introspection request is the resolved identity appended to the authorization JSON. -### OpenShift OAuth (user-echo endpoint) (`identity.openshift`) - - - - - -
Not implemented - In analysis
+### X.509 client certificate authentication (`authentication.x509`) -Online token introspection of OpenShift-valid access tokens based on OpenShift's user-echo endpoint. - -### Mutual Transport Layer Security (mTLS) authentication (`identity.mtls`) - -Authorino can verify x509 certificates presented by clients for authentication on the request to the protected APIs, at application level. +Authorino can verify X.509 certificates presented by clients for authentication on the request to the protected APIs, at application level. Trusted root Certificate Authorities (CA) are stored in Kubernetes Secrets labeled according to selectors specified in the AuthConfig, watched and indexed by Authorino. Make sure to create proper `kubernetes.io/tls`-typed Kubernetes Secrets, containing the public certificates of the CA stored in either a `tls.crt` or `ca.crt` entry inside the secret. -Trusted root CA secrets must be created in the same namespace of the `AuthConfig` (default) or `spec.identity.mtls.allNamespaces` must be set to `true` (only works with [cluster-wide Authorino instances](./architecture.md#cluster-wide-vs-namespaced-instances)). +Trusted root CA secrets must be created in the same namespace of the `AuthConfig` (default) or `spec.authentication.x509.allNamespaces` must be set to `true` (only works with [cluster-wide Authorino instances](./architecture.md#cluster-wide-vs-namespaced-instances)). The identity object resolved out of a client x509 certificate is equal to the subject field of the certificate, and it serializes as JSON within the Authorization JSON usually as follows: @@ -284,21 +272,11 @@ The identity object resolved out of a client x509 certificate is equal to the su } ``` -### Hash Message Authentication Code (HMAC) authentication (`identity.hmac`) - - - - - -
Not implemented - Planned (#9)
- -Authentication based on the validation of a hash code generated from the contextual information of the request to the protected API, concatenated with a secret known by the API consumer. - -### Plain (`identity.plain`) +### Plain (`authentication.plain`) Authorino can read plain identity objects, based on authentication tokens provided and verified beforehand using other means (e.g. Envoy [JWT Authentication filter](https://www.envoyproxy.io/docs/envoy/latest/configuration/http/http_filters/jwt_authn_filter#config-http-filters-jwt-authn), Kubernetes API server authentication), and injected into the payload to the external authorization service. -The plain identity object is retrieved from the Authorization JSON based on a [JSON path](#common-feature-json-paths-valuefromauthjson) specified in the `AuthConfig`. +The plain identity object is retrieved from the Authorization JSON based on a [JSON path](#common-feature-json-paths-selector) specified in the `AuthConfig`. This feature is particularly useful in cases where authentication/identity verification is handled before invoking the authorization service and its resolved value injected in the payload can be trusted. Examples of applications for this feature include: - Authentication handled in Envoy leveraging the Envoy JWT Authentication filter (decoded JWT injected as 'metadata_context') @@ -308,15 +286,15 @@ Example of `AuthConfig` to retrieve plain identity object from the Authorization ```yaml spec: - identity: - - name: plain - plain: - authJSON: context.metadata_context.filter_metadata.envoy\.filters\.http\.jwt_authn|verified_jwt + authentication: + "pre-validated-jwt": + plain: + selector: context.metadata_context.filter_metadata.envoy\.filters\.http\.jwt_authn|verified_jwt ``` If the specified JSON path does not exist in the Authorization JSON or the value is `null`, the identity verification will fail and, unless other identity config succeeds, Authorino will halt the Auth Pipeline with the usual `401 Unauthorized`. -### Anonymous access (`identity.anonymous`) +### Anonymous access (`authentication.anonymous`) Literally a no-op evaluator for the identity verification phase that returns a static identity object `{"anonymous":true}`. @@ -328,45 +306,64 @@ Example of `AuthConfig` spec that falls back to anonymous access when OIDC authe ```yaml spec: - identity: - - name: jwt - oidc: { endpoint: ... } - - name: anonymous - priority: 1 # expired oidc token, missing creds, etc. default to anonymous access - anonymous: {} + authentication: + "jwt": + jwt: + issuerUrl: "…" + "anonymous": + priority: 1 # expired oidc token, missing creds, etc. default to anonymous access + anonymous: {} authorization: - - name: read-only-access-if-authn-fails - when: - - selector: auth.identity.anonymous - operator: eq - value: "true" - json: - rules: - - selector: context.request.http.method - operator: eq - value: GET + "read-only-access-if-authn-fails": + when: + - selector: auth.identity.anonymous + operator: eq + value: "true" + patternMatching: + patterns: + - selector: context.request.http.method + operator: eq + value: GET ``` ### Festival Wristband authentication -Authorino-issued [Festival Wristband](#festival-wristband-tokens-responsewristband) tokens can be validated as any other signed JWT using Authorino's [OpenID Connect (OIDC) JWT/JOSE verification and validation](#openid-connect-oidc-jwtjose-verification-and-validation-identityoidc). +Authorino-issued [Festival Wristband](#festival-wristband-tokens-responsesuccessheadersdynamicmetadatawristband) tokens can be validated as any other signed JWT using Authorino's [JWT verification](#jwt-verification-authenticationjwt). The value of the issuer must be the same issuer specified in the custom resource for the protected API originally issuing wristband. Eventually, this can be the same custom resource where the wristband is configured as a valid source of identity, but not necessarily. -### _Extra:_ Auth credentials ([`credentials`](https://pkg.go.dev/github.com/kuadrant/authorino/api/v1beta1?utm_source=gopls#Credentials)) +### _Extra:_ Auth credentials ([`authentication.credentials`](https://pkg.go.dev/github.com/kuadrant/authorino/api/v1beta2?utm_source=gopls#Credentials)) All the identity verification methods supported by Authorino can be configured regarding the location where access tokens and credentials (i.e. authentication secrets) fly within the request. -By default, authentication secrets are expected to be supplied in the `Authorization` HTTP header, with the `Bearer` prefix and plain authentication secret, separated by space. The full list of supported options for the location of authentication secrets and selector is specified in the table below: +By default, authentication secrets are expected to be supplied in the `Authorization` HTTP header, with the default `Bearer` prefix and the plain authentication secret separated by space. + +The full list of supported options is exemplified below: + +```yaml +spec: + authentication: + "creds-in-the-authz-header": + credentials: + authorizationHeader: + prefix: JWT + + "creds-in-a-custom-header": + credentials: + customHeader: + name: X-MY-CUSTOM-HEADER + prefix: "" + + "creds-in-a-query-param": + queryString: + name: my_param -| Location (`credentials.in`) | Description | Selector (`credentials.keySelector`) | -|-----------------------------|-----------------------------|--------------------------------------------------| -| `authorization_header` | `Authorization` HTTP header | Prefix (default: `Bearer`) | -| `custom_header` | Custom HTTP header | Name of the header. Value should have no prefix. | -| `query` | Query string parameter | Name of the parameter | -| `cookie` | Cookie header | ID of the cookie entry | + "creds-in-a-cookie-entry": + cookie: + name: cookie-key +``` -### _Extra:_ Identity extension ([`extendedProperties`](https://pkg.go.dev/github.com/kuadrant/authorino/api/v1beta1?utm_source=gopls#Identity)) +### _Extra:_ Identity extension ([`authentication.defaults`](https://pkg.go.dev/github.com/kuadrant/authorino/api/v1beta2?utm_source=gopls#ExtendedProperties) and [`authentication.overrides`](https://pkg.go.dev/github.com/kuadrant/authorino/api/v1beta2?utm_source=gopls#ExtendedProperties)) Resolved identity objects can be extended with user-defined JSON properties. Values can be static or fetched from the Authorization JSON. @@ -376,9 +373,9 @@ In such cases, identity extension can be used to normalize the token to always i In case of extending an existing property of the identity object (replacing), the API allows to control whether to overwrite the value or not. This is particularly useful for normalizing tokens of a same identity source that nonetheless may occasionally differ in structure, such as in the case of JWT claims that sometimes may not be present but can be safely replaced with another (e.g. `username` or `sub`). -## External auth metadata features ([`metadata`](https://pkg.go.dev/github.com/kuadrant/authorino/api/v1beta1?utm_source=gopls#Metadata)) +## External auth metadata features ([`metadata`](https://pkg.go.dev/github.com/kuadrant/authorino/api/v1beta2?utm_source=gopls#Metadata)) -### HTTP GET/GET-by-POST ([`metadata.http`](https://pkg.go.dev/github.com/kuadrant/authorino/api/v1beta1?utm_source=gopls#Metadata_GenericHTTP)) +### HTTP GET/GET-by-POST ([`metadata.http`](https://pkg.go.dev/github.com/kuadrant/authorino/api/v1beta2?utm_source=gopls#HttpEndpointSpec)) Generic HTTP adapter that sends a request to an external service. It can be used to fetch external metadata for the authorization policies (phase ii of the Authorino [Auth Pipeline](./architecture.md#the-auth-pipeline-aka-enforcing-protection-in-request-time)), or as a web hook. @@ -388,21 +385,21 @@ POST request parameters as well as the encoding of the content can be controlled Authentication of Authorino with the external metadata server can be set either via long-lived shared secret stored in a Kubernetes Secret or via OAuth2 client credentials grant. For long-lived shared secret, set the `sharedSecretRef` field. For OAuth2 client credentials grant, use the `oauth2` option. -In both cases, the location where the secret (long-lived or OAuth2 access token) travels in the request performed to the external HTTP service can be specified in the [`credentials`](#extra-auth-credentials-credentials) field. By default, the authentication secret is supplied in the `Authorization` header with the `Bearer` prefix. +In both cases, the location where the secret (long-lived or OAuth2 access token) travels in the request performed to the external HTTP service can be specified in the [`credentials`](#extra-auth-credentials-authenticationcredentials) field. By default, the authentication secret is supplied in the `Authorization` header with the `Bearer` prefix. Custom headers can be set with the `headers` field. Nevertheless, headers such as `Content-Type` and `Authorization` (or eventual custom header used for carrying the authentication secret, set instead via the `credentials` option) will be superseded by the respective values defined for the fields `contentType` and `sharedSecretRef`. -### OIDC UserInfo ([`metadata.userInfo`](https://pkg.go.dev/github.com/kuadrant/authorino/api/v1beta1?utm_source=gopls#Metadata_UserInfo)) +### OIDC UserInfo ([`metadata.userInfo`](https://pkg.go.dev/github.com/kuadrant/authorino/api/v1beta2?utm_source=gopls#UserInfoMetadataSpec)) Online fetching of OpenID Connect (OIDC) UserInfo data (phase ii of the Authorino [Auth Pipeline](./architecture.md#the-auth-pipeline-aka-enforcing-protection-in-request-time)), associated with an OIDC identity source configured and resolved in phase (i). Apart from possibly complementing information of the JWT, fetching OpenID Connect UserInfo in request-time can be particularly useful for remote checking the state of the session, as opposed to only verifying the JWT/JWS offline. -Implementation requires an OpenID Connect issuer ([`spec.identity.oidc`](#openid-connect-oidc-jwtjose-verification-and-validation-identityoidc)) configured in the same `AuthConfig`. +Implementation requires a JWT verification authentication config ([`spec.authentication.jwt`](#jwt-verification-authenticationjwt)) in the same `AuthConfig`, so the well-known configuration of the OpenId Connect (OIDC) issuer can be reused. The response returned by the OIDC server to the UserInfo request is appended (as JSON) to `auth.metadata` in the authorization JSON. -### User-Managed Access (UMA) resource registry ([`metadata.uma`](https://pkg.go.dev/github.com/kuadrant/authorino/api/v1beta1?utm_source=gopls#Metadata_UMA)) +### User-Managed Access (UMA) resource registry ([`metadata.uma`](https://pkg.go.dev/github.com/kuadrant/authorino/api/v1beta2?utm_source=gopls#UmaMetadataSpec)) User-Managed Access (UMA) is an OAuth-based protocol for resource owners to allow other users to access their resources. Since the UMA-compliant server is expected to know about the resources, Authorino includes a client that fetches resource data from the server and adds that as metadata of the authorization payload. @@ -416,14 +413,14 @@ It's important to notice that Authorino does NOT manage resources in the UMA-com The resources data is added as metadata of the authorization payload and passed as input for the configured authorization policies. All resources returned by the UMA-compliant server in the query by URI are passed along. They are available in the PDPs (authorization payload) as `input.auth.metadata.custom-name => Array`. (See [The "Auth Pipeline"](./architecture.md#the-auth-pipeline-aka-enforcing-protection-in-request-time) for details.) -## Authorization features ([`authorization`](https://pkg.go.dev/github.com/kuadrant/authorino/api/v1beta1?utm_source=gopls#Authorization)) +## Authorization features ([`authorization`](https://pkg.go.dev/github.com/kuadrant/authorino/api/v1beta2?utm_source=gopls#Authorization)) -### JSON pattern-matching authorization rules ([`authorization.json`](https://pkg.go.dev/github.com/kuadrant/authorino/api/v1beta1?utm_source=gopls#Authorization_JSONPatternMatching)) +### Pattern-matching authorization ([`authorization.patternMatching`](https://pkg.go.dev/github.com/kuadrant/authorino/api/v1beta2?utm_source=gopls#PatternMatchingAuthorizationSpec)) -Grant/deny access based on simple pattern-matching expressions ("rules") compared against values selected from the Authorization JSON. +Grant/deny access based on simple pattern-matching expressions ("patterns") compared against values selected from the Authorization JSON. Each expression is a tuple composed of: -- a `selector`, to fetch from the Authorization JSON – see [Common feature: JSON paths](#common-feature-json-paths-valuefromauthjson) for details about syntax; +- a `selector`, to fetch from the Authorization JSON – see [Common feature: JSON paths](#common-feature-json-paths-selector) for details about syntax; - an `operator` – `eq` (_equals_), `neq` (_not equal_); `incl` (_includes_) and `excl` (_excludes_), for arrays; and `matches`, for regular expressions; - a fixed comparable `value` @@ -432,28 +429,28 @@ Rules can mix and combine literal expressions and references to expression sets ```yaml spec: authorization: - - name: my-simple-json-pattern-matching-policy - json: - rules: # All rules must match for access to be granted - - selector: auth.identity.email_verified - operator: eq - value: "true" - - patternRef: admin + "my-simple-json-pattern-matching-policy": + patternMatching: + patterns: # All patterns must match for access to be granted + - selector: auth.identity.email_verified + operator: eq + value: "true" + - patternRef: admin patterns: admin: # a named pattern that can be reused in other sets of rules or conditions - - selector: auth.identity.roles - operator: incl - value: admin + - selector: auth.identity.roles + operator: incl + value: admin ``` -### Open Policy Agent (OPA) Rego policies ([`authorization.opa`](https://pkg.go.dev/github.com/kuadrant/authorino/api/v1beta1?utm_source=gopls#Authorization_OPA)) +### Open Policy Agent (OPA) Rego policies ([`authorization.opa`](https://pkg.go.dev/github.com/kuadrant/authorino/api/v1beta2?utm_source=gopls#OpaAuthorizationSpec)) You can model authorization policies in [Rego language](https://www.openpolicyagent.org/docs/latest/policy-language/) and add them as part of the protection of your APIs. -Policies can be either declared in-line in Rego language (`inlineRego`) or as an HTTP endpoint where Authorino will fetch the source code of the policy in reconciliation-time (`externalRegistry`). +Policies can be either declared in-line in Rego language (`rego`) or as an HTTP endpoint where Authorino will fetch the source code of the policy in reconciliation-time (`externalPolicy`). -Policies pulled from external registries can be configured to be automatically refreshed (pulled again from the external registry), by setting the `authorization.opa.externalRegistry.ttl` field (given in seconds, default: `0` – i.e. auto-refresh disabled). +Policies pulled from external registries can be configured to be automatically refreshed (pulled again from the external registry), by setting the `authorization.opa.externalPolicy.ttl` field (given in seconds, default: `0` – i.e. auto-refresh disabled). Authorino's built-in OPA module precompiles the policies during reconciliation of the AuthConfig and caches the precompiled policies for fast evaluation in runtime, where they receive the Authorization JSON as input. @@ -461,7 +458,7 @@ Authorino's built-in OPA module precompiles the policies during reconciliation o An optional field `allValues: boolean` makes the values of all rules declared in the Rego document to be returned in the OPA output after policy evaluation. When disabled (default), only the boolean value `allow` is returned. Values of internal rules of the Rego document can be referenced in subsequent policies/phases of the Auth Pipeline. -### Kubernetes SubjectAccessReview ([`authorization.kubernetes`](https://pkg.go.dev/github.com/kuadrant/authorino/api/v1beta1?utm_source=gopls#Authorization_KubernetesAuthz)) +### Kubernetes SubjectAccessReview ([`authorization.kubernetesSubjectAccessReview`](https://pkg.go.dev/github.com/kuadrant/authorino/api/v1beta2?utm_source=gopls#KubernetesSubjectAccessReviewAuthorizationSpec)) Access control enforcement based on rules defined in the Kubernetes authorization system, i.e. `Role`, `ClusterRole`, `RoleBinding` and `ClusterRoleBinding` resources of Kubernetes RBAC. @@ -500,16 +497,15 @@ Kubernetes' authorization policy configs look like the following in an Authorino ```yaml authorization: - - name: kubernetes-rbac - kubernetes: - user: - valueFrom: # values of the parameter can be fixed (`value`) or fetched from the Authorization JSON (`valueFrom.authJSON`) - authJSON: auth.identity.metadata.annotations.userid + "kubernetes-rbac": + kubernetesSubjectAccessReview: + user: # values of the parameter can be fixed (`value`) or fetched from the Authorization JSON (`selector`) + selector: auth.identity.metadata.annotations.userid groups: [] # user groups to test for. # for resource attributes permission checks; omit it to perform a non-resource attributes SubjectAccessReview with path and method/verb assumed from the original request - # if included, use the resource attributes, where the values for each parameter can be fixed (`value`) or fetched from the Authorization JSON (`valueFrom.authJSON`) + # if included, use the resource attributes, where the values for each parameter can be fixed (`value`) or fetched from the Authorization JSON (`selector`) resourceAttributes: namespace: value: default @@ -518,117 +514,105 @@ authorization: resource: value: pets # the resource kind name: - valueFrom: { authJSON: context.request.http.path.@extract:{"sep":"/","pos":2} } # resource name – e.g., the {id} in `/pets/{id}` + selector: context.request.http.path.@extract:{"sep":"/","pos":2} # resource name – e.g., the {id} in `/pets/{id}` verb: - valueFrom: { authJSON: context.request.http.method.@case:lower } # api operation – e.g., copying from the context to use the same http method of the request + selector: context.request.http.method.@case:lower # api operation – e.g., copying from the context to use the same http method of the request ``` `user` and properties of `resourceAttributes` can be defined from fixed values or patterns of the Authorization JSON. An array of `groups` (optional) can as well be set. When defined, it will be used in the `SubjectAccessReview` request. -### Authzed/SpiceDB ([`authorization.authzed`](https://pkg.go.dev/github.com/kuadrant/authorino/api/v1beta1?utm_source=gopls#Authorization_Authzed)) +### SpiceDB ([`authorization.spicedb`](https://pkg.go.dev/github.com/kuadrant/authorino/api/v1beta2?utm_source=gopls#SpiceDBAuthorizationSpec)) -Check permission requests sent to a Google Zanzibar-based [Authzed/SpiceDB](https://authzed.com) instance, via gRPC. +Check permission requests via gRPC with an external Google Zanzibar-inspired [SpiceDB](https://authzed.com) server, by Authzed. Subject, resource and permission parameters can be set to static values or read from the Authorization JSON. ```yaml spec: authorization: - - name: authzed - authzed: - endpoint: spicedb:50051 - insecure: true # disables TLS - sharedSecretRef: - name: spicedb - key: token - subject: - kind: - value: blog/user - name: - valueFrom: - authJSON: auth.identity.sub - resource: - kind: - value: blog/post - name: - valueFrom: - authJSON: context.request.http.path.@extract:{"sep":"/","pos":2} # /posts/{id} - permission: - valueFrom: - authJSON: context.request.http.method + "spicedb": + spicedb: + endpoint: spicedb:50051 + insecure: true # disables TLS + sharedSecretRef: + name: spicedb + key: token + subject: + kind: + value: blog/user + name: + selector: auth.identity.sub + resource: + kind: + value: blog/post + name: + selector: context.request.http.path.@extract:{"sep":"/","pos":2} # /posts/{id} + permission: + selector: context.request.http.method ``` -### Keycloak Authorization Services (UMA-compliant Authorization API) +## Custom response features ([`response`](https://pkg.go.dev/github.com/kuadrant/authorino/api/v1beta2?utm_source=gopls#Response)) - - - - -
Not implemented - In analysis
+### Custom response forms: successful authorization vs custom denial status -Online delegation of authorization to a Keycloak server. +The response to the external authorization request can be customized in the following fashion: +- Successful authorization (`response.success`) + - Added HTTP headers (`response.success.headers`) + - Envoy Dynamic Metadata (`response.success.dynamicMetadata`) +- Custom denial status + - Unauthenticated (`response.unauthenticated`) + - Unauthorized (`response.unauthorized`) -## Dynamic response features ([`response`](https://pkg.go.dev/github.com/kuadrant/authorino/api/v1beta1?utm_source=gopls#Response)) +Successful authorization custom responses can be set based on any of the supported custom authorization methods: +- Plain text value +- JSON injection +- Festival Wristband Tokens -### JSON injection ([`response.json`](https://pkg.go.dev/github.com/kuadrant/authorino/api/v1beta1?utm_source=gopls#Response_DynamicJSON)) +#### Added HTTP headers + +Set custom responses as HTTP headers injected in the request post-successful authorization by specifying one of the supported methods under `response.success.headers`. + +The name of the response config (default) or the value of the `key` option (if provided) will used as the name of the header. + +#### Envoy Dynamic Metadata + +Authorino custom response methods can also be used to propagate [Envoy Dynamic Metadata](https://www.envoyproxy.io/docs/envoy/latest/configuration/advanced/well_known_dynamic_metadata). To do so, set one of the supported methods under `response.success.dynamicMetadata`. + +The name of the response config (default) or the value of the `key` option (if provided) will used as the name of the root property of the dynamic metadata content. -User-defined dynamic JSON objects generated by Authorino in the response phase, from static or dynamic data of the auth pipeline, and passed back to the external authorization client within added HTTP headers or as Envoy [Well Known Dynamic Metadata](https://www.envoyproxy.io/docs/envoy/latest/configuration/advanced/well_known_dynamic_metadata). +A custom response exported as Envoy Dynamic Metadata can be set in the Envoy route or virtual host configuration as input to a consecutive filter in the filter chain. -The following Authorino `AuthConfig` custom resource is an example that defines 3 dynamic JSON response items, where two items are returned to the client, stringified, in added HTTP headers, and the third is wrapped as Envoy Dynamic Metadata("emitted", in Envoy terminology). Envoy proxy can be configured to "pipe" dynamic metadata emitted by one filter into another filter – for example, from external authorization to rate limit. +E.g., to read metadata emitted by the authorization service with scheme `{ "auth-data": { "api-key-ns": string, "api-key-name": string } }`, as input in a rate limit configuration placed in the filter chain after the external authorization, the Envoy config may look like the following: ```yaml -apiVersion: authorino.kuadrant.io/v1beta1 -kind: AuthConfig -metadata: - namespace: my-namespace - name: my-api-protection -spec: - hosts: - - my-api.io - identity: - - name: edge - apiKey: - selector: - matchLabels: - authorino.kuadrant.io/managed-by: authorino - credentials: - in: authorization_header - keySelector: APIKEY - response: - - name: a-json-returned-in-a-header - wrapper: httpHeader # can be omitted - wrapperKey: x-my-custom-header # if omitted, name of the header defaults to the name of the config ("a-json-returned-in-a-header") - json: - properties: - - name: prop1 - value: value1 - - name: prop2 - valueFrom: - authJSON: some.path.within.auth.json - - - name: another-json-returned-in-a-header - wrapperKey: x-ext-auth-other-json - json: - properties: - - name: propX - value: valueX - - - name: a-json-returned-as-envoy-metadata - wrapper: envoyDynamicMetadata - wrapperKey: auth-data - json: - properties: - - name: api-key-ns - valueFrom: - authJSON: auth.identity.metadata.namespace - - name: api-key-name - valueFrom: - authJSON: auth.identity.metadata.name +# Envoy config snippet to inject `user_namespace` and `username` rate limit descriptors from metadata emitted by Authorino +rate_limits: +- actions: + - metadata: + metadata_key: + key: "envoy.filters.http.ext_authz" + path: + - key: auth-data # root of the dynamic metadata object, as declared in a custom response config of the AuthConfig (name or key) + - key: api-key-ns + descriptor_key: user_namespace + - metadata: + metadata_key: + key: "envoy.filters.http.ext_authz" + path: + - key: auth-data # root of the dynamic metadata object, as declared in a custom response config of the AuthConfig (name or key) + - key: api-key-name + descriptor_key: username ``` -### Plain ([`response.plain`](https://pkg.go.dev/github.com/kuadrant/authorino/api/v1beta1?utm_source=gopls#Response_Plain)) +#### Custom denial status ([`response.unauthenticated`](https://pkg.go.dev/github.com/kuadrant/authorino/api/v1beta2?utm_source=gopls#DenyWithSpec) and [`response.unauthorized`](https://pkg.go.dev/github.com/kuadrant/authorino/api/v1beta2?utm_source=gopls#DenyWithSpec)) + +By default, Authorino will inform Envoy to respond with `401 Unauthorized` or `403 Forbidden` respectively when the identity verification (phase i of the [Auth Pipeline](./architecture.md#the-auth-pipeline-aka-enforcing-protection-in-request-time)) or authorization (phase ii) fail. These can be customized respectively by specifying `spec.response.unauthanticated` and `spec.response.unauthorized` in the `AuthConfig`. + +### Custom response methods + +#### Plain text ([`response.success..plain`](https://pkg.go.dev/github.com/kuadrant/authorino/api/v1beta2?utm_source=gopls#PlainAuthResponseSpec)) Simpler, yet more generalized form, for extending the authorization response for header mutation and Envoy Dynamic Metadata, based on plain text values. @@ -636,62 +620,115 @@ The value can be static: ```yaml response: -- name: x-auth-service - plain: - value: Authorino + success: + headers: + "x-auth-service" + plain: + value: Authorino ``` or fetched dynamically from the [Authorization JSON](./architecture.md#the-authorization-json) (which includes support for [interpolation](#interpolation)): ```yaml -- name: x-username - plain: - valueFrom: - authJSON: auth.identity.username +response: + success: + headers: + "x-username": + plain: + selector: auth.identity.username ``` -### Festival Wristband tokens ([`response.wristband`](https://pkg.go.dev/github.com/kuadrant/authorino/api/v1beta1?utm_source=gopls#Response_Wristband)) +#### JSON injection ([`response.success..json`](https://pkg.go.dev/github.com/kuadrant/authorino/api/v1beta2?utm_source=gopls#JsonAuthResponseSpec)) + +User-defined dynamic JSON objects generated by Authorino in the response phase, from static or dynamic data of the auth pipeline, and passed back to the external authorization client within added HTTP headers or Dynamic Metadata. + +The following Authorino `AuthConfig` custom resource is an example that defines 3 dynamic JSON response items, where two items are returned to the client, stringified, in added HTTP headers, and the third as Envoy Dynamic Metadata. Envoy proxy can be configured to propagate the dynamic metadata emitted by Authorino into another filter – e.g. the rate limit filter. + +```yaml +apiVersion: authorino.kuadrant.io/v1beta2 +kind: AuthConfig +metadata: + namespace: my-namespace + name: my-api-protection +spec: + hosts: + - my-api.io + authentication: + "edge": + apiKey: + selector: + matchLabels: + authorino.kuadrant.io/managed-by: authorino + credentials: + authorizationHeader: + prefix: APIKEY + response: + success: + headers: + "x-my-custom-header": + json: + properties: + "prop1": + value: value1 + "prop2": + selector: some.path.within.auth.json + "x-ext-auth-other-json": + json: + properties: + "propX": + value: valueX + + dynamicMetadata: + "auth-data": + json: + properties: + "api-key-ns": + seletor: auth.identity.metadata.namespace + "api-key-name": + selector: auth.identity.metadata.name +``` + +#### Festival Wristband tokens ([`response.success..wristband`](https://pkg.go.dev/github.com/kuadrant/authorino/api/v1beta2?utm_source=gopls#WristbandAuthResponseSpec)) Festival Wristbands are signed OpenID Connect JSON Web Tokens (JWTs) issued by Authorino at the end of the auth pipeline and passed back to the client, typically in added HTTP response header. It is an opt-in feature that can be used to implement Edge Authentication Architecture (EAA) and enable token normalization. Authorino wristbands include minimal standard JWT claims such as `iss`, `iat`, and `exp`, and optional user-defined custom claims, whose values can be static or dynamically fetched from the authorization JSON. The Authorino `AuthConfig` custom resource below sets an API protection that issues a wristband after a successful authentication via API key. Apart from standard JWT claims, the wristband contains 2 custom claims: a static value `aud=internal` and a dynamic value `born` that fetches from the authorization JSON the date/time of creation of the secret that represents the API key used to authenticate. ```yaml -apiVersion: authorino.kuadrant.io/v1beta1 +apiVersion: authorino.kuadrant.io/v1beta2 kind: AuthConfig metadata: namespace: my-namespace name: my-api-protection spec: hosts: - - my-api.io - identity: - - name: edge + - my-api.io + authentication: + "edge": apiKey: selector: matchLabels: authorino.kuadrant.io/managed-by: authorino credentials: - in: authorization_header - keySelector: APIKEY + authorizationHeader: + prefix: APIKEY response: - - name: my-wristband - wristband: - issuer: https://authorino-oidc.default.svc:8083/my-namespace/my-api-protection/my-wristband - customClaims: - - name: aud - value: internal - - name: born - valueFrom: - authJSON: auth.identity.metadata.creationTimestamp - tokenDuration: 300 - signingKeyRefs: - - name: my-signing-key - algorithm: ES256 - - name: my-old-signing-key - algorithm: RS256 - wrapper: httpHeader # can be omitted - wrapperKey: x-ext-auth-wristband # whatever http header name desired - defaults to the name of the response config ("my-wristband") + success: + headers: + "x-wristband": + wristband: + issuer: https://authorino-oidc.default.svc:8083/my-namespace/my-api-protection/x-wristband + customClaims: + "aud": + value: internal + "born": + selector: auth.identity.metadata.creationTimestamp + tokenDuration: 300 + signingKeyRefs: + - name: my-signing-key + algorithm: ES256 + - name: my-old-signing-key + algorithm: RS256 ``` The signing key names listed in `signingKeyRefs` must match the names of Kubernetes `Secret` resources created in the same namespace, where each secret contains a `key.pem` entry that holds the value of the private key that will be used to sign the wristbands issued, formatted as [PEM](https://en.wikipedia.org/wiki/Privacy-Enhanced_Mail). The first key in this list will be used to sign the wristbands, while the others are kept to support key rotation. @@ -702,44 +739,6 @@ For each protected API configured for the Festival Wristband issuing, Authorino - **JSON Web Key Set (JWKS) well-known endpoint:**
https://authorino-oidc.default.svc:8083/{namespace}/{api-protection-name}/{response-config-name}/.well-known/openid-connect/certs -### _Extra:_ Response wrappers ([`wrapper`](https://pkg.go.dev/github.com/kuadrant/authorino/api/v1beta1?utm_source=gopls#Response_Wrapper) and [`wrapperKey`](https://pkg.go.dev/github.com/kuadrant/authorino/api/v1beta1?utm_source=gopls#Response_Wrapper)) - -#### Added HTTP headers - -By default, Authorino dynamic responses (injected JSON and Festival Wristband tokens) are passed back to Envoy, stringified, as injected HTTP headers. This can be made explicit by setting the `wrapper` property of the response config to `httpHeader`. - -The property `wrapperKey` controls the name of the HTTP header, with default to the name of dynamic response config when omitted. - -#### Envoy Dynamic Metadata - -Authorino dynamic responses (injected JSON and Festival Wristband tokens) can be passed back to Envoy in the form of Envoy Dynamic Metadata. To do so, set the `wrapper` property of the response config to `envoyDynamicMetadata`. - -A response config with `wrapper=envoyDynamicMetadata` and `wrapperKey=auth-data` in the `AuthConfig` can be configured in the Envoy route or virtual host setting to be passed to rate limiting filter as below. The metadata content is expected to be a dynamic JSON injected by Authorino containing `{ "auth-data": { "api-key-ns": string, "api-key-name": string } }`. (See the response config `a-json-returned-as-envoy-metadata` in the example for the [JSON injection feature](#json-injection-responsejson) above) - -```yaml -# Envoy config snippet to inject `user_namespace` and `username` rate limit descriptors from metadata returned by Authorino -rate_limits: -- actions: - - metadata: - metadata_key: - key: "envoy.filters.http.ext_authz" - path: - - key: auth-data - - key: api-key-ns - descriptor_key: user_namespace - - metadata: - metadata_key: - key: "envoy.filters.http.ext_authz" - path: - - key: auth-data - - key: api-key-name - descriptor_key: username -``` - -### _Extra:_ Custom denial status ([`denyWith`](https://pkg.go.dev/github.com/kuadrant/authorino/api/v1beta1?utm_source=gopls#DenyWith)) - -By default, Authorino will inform Envoy to respond with `401 Unauthorized` or `403 Forbidden` respectively when the identity verification (phase i of the [Auth Pipeline](./architecture.md#the-auth-pipeline-aka-enforcing-protection-in-request-time)) or authorization (phase ii) fail. These can be customized by specifying `spec.denyWith` in the `AuthConfig`. - ## Callbacks (`callbacks`) ### HTTP endpoints (`callbacks.http`) @@ -752,25 +751,24 @@ Example: ```yaml spec: - identity: […] + authentication: […] authorization: […] callbacks: - - name: log + "log": http: - endpoint: http://logsys + url: http://logsys method: POST body: - valueFrom: - authJSON: | + selector: | \{"requestId":context.request.http.id,"username":"{auth.identity.username}","authorizationResult":{auth.authorization}\} - - name: important-forbidden + "important-forbidden": when: - - selector: auth.authorization.important-policy - operator: eq - value: "false" + - selector: auth.authorization.important-policy + operator: eq + value: "false" http: - endpoint: "http://monitoring/important?forbidden-user={auth.identity.username}" + url: "http://monitoring/important?forbidden-user={auth.identity.username}" ``` ## Common feature: Priorities @@ -791,83 +789,79 @@ Priorities can be set using the `priority` property available in all evaluator c Consider the following example to understand how priorities work: ```yaml -apiVersion: authorino.kuadrant.io/v1beta1 +apiVersion: authorino.kuadrant.io/v1beta2 kind: AuthConfig metadata: name: talker-api-protection spec: hosts: - - talker-api - identity: - - name: tier-1 + - talker-api + authentication: + "tier-1": priority: 0 apiKey: selector: matchLabels: tier: "1" - - name: tier-2 + "tier-2": priority: 1 apiKey: selector: matchLabels: tier: "2" - - name: tier-3 + "tier-3": priority: 1 apiKey: selector: matchLabels: tier: "3" metadata: - - name: first + "first": http: - endpoint: http://talker-api:3000 - method: GET - - name: second + url: http://talker-api:3000 + "second": priority: 1 http: - endpoint: http://talker-api:3000/first_uuid={auth.metadata.first.uuid} - method: GET + url: http://talker-api:3000/first_uuid={auth.metadata.first.uuid} authorization: - - name: allowed-endpoints + "allowed-endpoints": when: - - selector: context.request.http.path - operator: neq - value: /hi - - selector: context.request.http.path - operator: neq - value: /hello - - selector: context.request.http.path - operator: neq - value: /aloha - - selector: context.request.http.path - operator: neq - value: /ciao - json: - rules: - - selector: deny - operator: eq - value: "true" - - name: more-expensive-policy # no point in evaluating this one if it's not an allowed endpoint + - selector: context.request.http.path + operator: neq + value: /hi + - selector: context.request.http.path + operator: neq + value: /hello + - selector: context.request.http.path + operator: neq + value: /aloha + - selector: context.request.http.path + operator: neq + value: /ciao + patternMatching: + patterns: + - selector: deny + operator: eq + value: "true" + "more-expensive-policy": # no point in evaluating this one if it's not an allowed endpoint priority: 1 opa: - inlineRego: | + rego: | allow { true } response: - - name: x-auth-data - json: - properties: - - name: tier - valueFrom: - authJSON: auth.identity.metadata.labels.tier - - name: first-uuid - valueFrom: - authJSON: auth.metadata.first.uuid - - name: second-uuid - valueFrom: - authJSON: auth.metadata.second.uuid - - name: second-path - valueFrom: - authJSON: auth.metadata.second.path + success: + headers: + "x-auth-data": + json: + properties: + "tier": + selector: auth.identity.metadata.labels.tier + "first-uuid": + selector: auth.metadata.first.uuid + "second-uuid": + selector: auth.metadata.second.uuid + "second-path": + selector: auth.metadata.second.path ``` For the `AuthConfig` above, @@ -885,7 +879,7 @@ _Conditions_, named `when` in the AuthConfig API, are sets of expressions (JSON The scope for a set of `when` conditions can be the entire `AuthConfig` ("top-level conditions") or a particular evaluator of any phase of the auth pipeline. Each expression is a tuple composed of: -- a `selector`, to fetch from the Authorization JSON – see [Common feature: JSON paths](#common-feature-json-paths-valuefromauthjson) for details about syntax; +- a `selector`, to fetch from the Authorization JSON – see [Common feature: JSON paths](#common-feature-json-paths-selector) for details about syntax; - an `operator` – `eq` (_equals_), `neq` (_not equal_); `incl` (_includes_) and `excl` (_excludes_), for arrays; and `matches`, for regular expressions; - a fixed comparable `value` @@ -908,32 +902,32 @@ ii) to skip parts of an `AuthConfig` (i.e. a specific evaluator): ```yaml spec: metadata: - - name: metadata-source - http: - endpoint: https://my-metadata-source.io - when: # only fetch the external metadata if the context is HTTP method other than OPTIONS - - selector: context.request.http.method - operator: neq - value: OPTIONS + "metadata-source": + http: + url: https://my-metadata-source.io + when: # only fetch the external metadata if the context is HTTP method other than OPTIONS + - selector: context.request.http.method + operator: neq + value: OPTIONS ``` iii) to enforce a particular evaluator only in certain contexts (really the same as the above, though to a different use case): ```yaml spec: - identity: - - name: authn-meth-1 - apiKey: {...} # this authn method only valid for POST requests to /foo[/*] - when: - - selector: context.request.http.path - operator: matches - value: ^/foo(/.*)?$ - - selector: context.request.http.method - operator: eq - value: POST + authentication: + "authn-meth-1": + apiKey: {…} # this authn method only valid for POST requests to /foo[/*] + when: + - selector: context.request.http.path + operator: matches + value: ^/foo(/.*)?$ + - selector: context.request.http.method + operator: eq + value: POST - - name: authn-meth-2 - oidc: {...} + "authn-meth-2": + jwt: {…} ``` iv) to avoid repetition while defining patterns for conditions: @@ -947,19 +941,19 @@ spec: value: ^/pets/\d+(/.*)$ metadata: - - name: pets-info - when: - - patternRef: a-pet - http: - endpoint: https://pets-info.io?petId={context.request.http.path.@extract:{"sep":"/","pos":2}} + "pets-info": + when: + - patternRef: a-pet + http: + url: https://pets-info.io?petId={context.request.http.path.@extract:{"sep":"/","pos":2}} authorization: - - name: pets-owners-only - when: - - patternRef: a-pet - opa: - inlineRego: | - allow { input.metadata["pets-info"].ownerid == input.auth.identity.userid } + "pets-owners-only": + when: + - patternRef: a-pet + opa: + rego: | + allow { input.metadata["pets-info"].ownerid == input.auth.identity.userid } ``` v) mixing and combining literal expressions and refs: @@ -979,30 +973,30 @@ spec: value: "" authorization: - - name: my-policy-1 - when: # authenticated access to /foo controlled by policy - - patternRef: foo - json: {...} + "my-policy-1": + when: # authenticated access to /foo controlled by policy + - patternRef: foo + patternMatching: {…} ``` vi) to avoid evaluating unnecessary identity checks when the user can indicate the preferred authentication method (again the pattern of skipping based upon the context): ```yaml spec: - identity: - - name: jwt - when: - - selector: context.request.http.headers.authorization - operator: matches - value: JWT .+ - oidc: {...} + authentication: + "jwt": + when: + - selector: context.request.http.headers.authorization + operator: matches + value: JWT .+ + jwt: {…} - - name: api-key - when: - - selector: context.request.http.headers.authorization - operator: matches - value: APIKEY .+ - apiKey: {...} + "api-key": + when: + - selector: context.request.http.headers.authorization + operator: matches + value: APIKEY .+ + apiKey: {…} ``` ## Common feature: Caching (`cache`) @@ -1018,27 +1012,26 @@ spec: hosts: - my-api.io - identity: [...] + authentication: […] metadata: - - name: external-metadata - http: - endpoint: http://my-external-source?search={context.request.http.path} - cache: - key: - valueFrom: { authJSON: context.request.http.path } - ttl: 300 + "external-metadata": + http: + url: http://my-external-source?search={context.request.http.path} + cache: + key: + selector: context.request.http.path + ttl: 300 authorization: - - name: complex-policy - opa: - externalRegistry: - endpoint: http://my-policy-registry - cache: - key: - valueFrom: - authJSON: "{auth.identity.group}-{context.request.http.method}-{context.request.http.path}" - ttl: 60 + "complex-policy": + opa: + externalPolicy: + url: http://my-policy-registry + cache: + key: + selector: "{auth.identity.group}-{context.request.http.method}-{context.request.http.path}" + ttl: 60 ``` The example above sets caching for the 'external-metadata' metadata config and for the 'complex-policy' authorization policy. In the case of 'external-metadata', the cache key is the path of the original HTTP request being authorized by Authorino (fetched dynamically from the [Authorization JSON](./architecture.md#the-authorization-json)); i.e., after obtaining a metadata object from the external source for a given contextual HTTP path one first time, whenever that same HTTP path repeats in a subsequent request, Authorino will use the cached object instead of sending a request again to the external source of metadata. After 5 minutes (300 seconds), the cache entry will expire and Authorino will fetch again from the source if requested. @@ -1058,42 +1051,43 @@ By default, Authorino will only export metrics down to the level of the AuthConf E.g.: ```yaml -apiVersion: authorino.kuadrant.io/v1beta1 +apiVersion: authorino.kuadrant.io/v1beta2 kind: AuthConfig metadata: name: my-authconfig namespace: my-ns spec: metadata: - - name: my-external-metadata - http: - endpoint: http://my-external-source?search={context.request.http.path} - metrics: true + "my-external-metadata": + http: + url: http://my-external-source?search={context.request.http.path} + metrics: true ``` The above will enable the metrics `auth_server_evaluator_duration_seconds` (histogram) and `auth_server_evaluator_total` (counter) with labels `namespace="my-ns"`, `authconfig="my-authconfig"`, `evaluator_type="METADATA_GENERIC_HTTP"` and `evaluator_name="my-external-metadata"`. The same pattern works for other types of evaluators. Find below the list of all types and corresponding label constant used in the metric: -| Evaluator type | Metric's `evaluator_type` label | -|----------------------------|---------------------------------| -| `identity.apiKey` | IDENTITY_APIKEY | -| `identity.kubernetes` | IDENTITY_KUBERNETES | -| `identity.oidc` | IDENTITY_OIDC | -| `identity.oauth2` | IDENTITY_OAUTH2 | -| `identity.mtls` | IDENTITY_MTLS | -| `identity.hmac` | IDENTITY_HMAC | -| `identity.plain` | IDENTITY_PLAIN | -| `identity.anonymous` | IDENTITY_NOOP | -| `metadata.http` | METADATA_GENERIC_HTTP | -| `metadata.userInfo` | METADATA_USERINFO | -| `metadata.uma` | METADATA_UMA | -| `authorization.json` | AUTHORIZATION_JSON | -| `authorization.opa` | AUTHORIZATION_OPA | -| `authorization.kubernetes` | AUTHORIZATION_KUBERNETES | -| `response.json` | RESPONSE_JSON | -| `response.wristband` | RESPONSE_WRISTBAND | - -Metrics at the level of the evaluators can also be enforced to an entire Authorino instance, by setting the --deep-metrics-enabled command-line flag. In this case, regardless of the value of the field `spec.(identity|metadata|authorization|response).metrics` in the AuthConfigs, individual metrics for all evaluators of all AuthConfigs will be exported. +| Evaluator type | Metric's `evaluator_type` label | +|-----------------------------------------------|---------------------------------| +| `authentication.apiKey` | IDENTITY_APIKEY | +| `authentication.kubernetesTokenReview` | IDENTITY_KUBERNETES | +| `authentication.jwt` | IDENTITY_OIDC | +| `authentication.oauth2Introspection` | IDENTITY_OAUTH2 | +| `authentication.x509` | IDENTITY_MTLS | +| `authentication.plain` | IDENTITY_PLAIN | +| `authentication.anonymous` | IDENTITY_NOOP | +| `metadata.http` | METADATA_GENERIC_HTTP | +| `metadata.userInfo` | METADATA_USERINFO | +| `metadata.uma` | METADATA_UMA | +| `authorization.patternMatching` | AUTHORIZATION_JSON | +| `authorization.opa` | AUTHORIZATION_OPA | +| `authorization.kubernetesSubjectAccessReview` | AUTHORIZATION_KUBERNETES | +| `authorization.spicedb` | AUTHORIZATION_AUTHZED | +| `response.success..plain` | RESPONSE_PLAIN | +| `response.success..json` | RESPONSE_JSON | +| `response.success..wristband` | RESPONSE_WRISTBAND | + +Metrics at the level of the evaluators can also be enforced to an entire Authorino instance, by setting the --deep-metrics-enabled command-line flag. In this case, regardless of the value of the field `spec.(authentication|metadata|authorization|response).metrics` in the AuthConfigs, individual metrics for all evaluators of all AuthConfigs will be exported. For more information about metrics exported by Authorino, see [Observability](./user-guides/observability.md#metrics). diff --git a/docs/getting-started.md b/docs/getting-started.md index 2acffc18..34dbb85b 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -51,11 +51,13 @@ Check out the [Feature specification](./features.md) page for more feature-speci The simplest way to install the Authorino Operator is by applying the manifest bundle: ```sh -kubectl apply -f https://raw.githubusercontent.com/Kuadrant/authorino-operator/main/config/deploy/manifests.yaml +curl -sL https://raw.githubusercontent.com/Kuadrant/authorino-operator/main/utils/install.sh | bash -s ``` The above will install the latest build of the Authorino Operator and latest version of the manifests (CRDs and RBAC), which by default points as well to the latest build of Authorino, both based on the `main` branches of each component. To install a stable released version of the Operator and therefore also defaults to its latest compatible stable release of Authorino, replace `main` with another tag of a proper release of the Operator, e.g. 'v0.2.0'. +This step will also install [cert-manager](https://github.com/jetstack/cert-manager) in the cluster (required). + Alternatively, you can deploy the Authorino Operator using the Operator Lifecycle Manager bundles. For instructions, check out [Installing via OLM](https://github.com/kuadrant/authorino-operator#installing-via-olm). ### Step: Request an Authorino instance @@ -72,12 +74,7 @@ The instructions here are for centralized gateway or centralized authorization s kubectl create namespace authorino ``` - Deploy [cert-manager](https://github.com/jetstack/cert-manager) (skip if you already have certificates and certificate keys created and stored in Kubernetes `Secret`s in the namespace or cert-manager is installed and running in the cluster): - ```sh - kubectl apply -f https://github.com/jetstack/cert-manager/releases/download/v1.4.0/cert-manager.yaml - ``` - - Create the TLS certificates (skip if you already have certificates and certificate keys created and stored in Kubernetes `Secret`s in the namespace): + Create the TLS certificates (requires [cert-manager](https://github.com/jetstack/cert-manager); skip if you already have certificates and certificate keys created and stored in Kubernetes `Secret`s in the namespace): ```sh curl -sSL https://raw.githubusercontent.com/Kuadrant/authorino/main/deploy/certs.yaml | sed "s/\$(AUTHORINO_INSTANCE)/authorino/g;s/\$(NAMESPACE)/authorino/g" | kubectl -n authorino apply -f - ``` @@ -138,12 +135,7 @@ The instructions here are for centralized gateway or centralized authorization s kubectl create namespace myapp ``` - Deploy [cert-manager](https://github.com/jetstack/cert-manager) (skip if you already have certificates and certificate keys created and stored in Kubernetes `Secret`s in the namespace or cert-manager is installed and running in the cluster): - ```sh - kubectl apply -f https://github.com/jetstack/cert-manager/releases/download/v1.4.0/cert-manager.yaml - ``` - - Create the TLS certificates (skip if you already have certificates and certificate keys created and stored in Kubernetes `Secret`s in the namespace): + Create the TLS certificates (requires [cert-manager](https://github.com/jetstack/cert-manager); skip if you already have certificates and certificate keys created and stored in Kubernetes `Secret`s in the namespace): ```sh curl -sSL https://raw.githubusercontent.com/Kuadrant/authorino/main/deploy/certs.yaml | sed "s/\$(AUTHORINO_INSTANCE)/authorino/g;s/\$(NAMESPACE)/myapp/g" | kubectl -n myapp apply -f - ``` @@ -346,25 +338,25 @@ For authentication based on OpenID Connect (OIDC) JSON Web Tokens (JWT), plus on ```sh kubectl -n myapp apply -f -< Authorino features in this guide: @@ -28,7 +28,7 @@ kind create cluster --name authorino-tutorial ## 1. Install the Authorino Operator ```sh -kubectl apply -f https://raw.githubusercontent.com/Kuadrant/authorino-operator/main/config/deploy/manifests.yaml +curl -sL https://raw.githubusercontent.com/Kuadrant/authorino-operator/main/utils/install.sh | bash -s ``` ## 2. Deploy the Talker API @@ -79,16 +79,16 @@ kubectl port-forward deployment/envoy 8000:8000 & ```sh kubectl apply -f -< Authorino features in this guide:
    -
  • Identity verification & authentication → API key
  • +
  • Identity verification & authentication → API key
@@ -32,7 +32,7 @@ kind create cluster --name authorino-tutorial ## 1. Install the Authorino Operator ```sh -kubectl apply -f https://raw.githubusercontent.com/Kuadrant/authorino-operator/main/config/deploy/manifests.yaml +curl -sL https://raw.githubusercontent.com/Kuadrant/authorino-operator/main/utils/install.sh | bash -s ``` ## 2. Deploy the Talker API @@ -83,22 +83,22 @@ kubectl port-forward deployment/envoy 8000:8000 & ```sh kubectl apply -f -<Authorino features in this guide: @@ -34,7 +34,7 @@ kind create cluster --name authorino-tutorial ## 1. Install the Authorino Operator ```sh -kubectl apply -f https://raw.githubusercontent.com/Kuadrant/authorino-operator/main/config/deploy/manifests.yaml +curl -sL https://raw.githubusercontent.com/Kuadrant/authorino-operator/main/utils/install.sh | bash -s ``` ## 2. Deploy the Talker API @@ -95,37 +95,37 @@ kubectl port-forward deployment/envoy 8000:8000 & ```sh kubectl apply -f -< Authorino features in this guide:
@@ -27,7 +27,7 @@ kind create cluster --name authorino-tutorial ## 1. Install the Authorino Operator ```sh -kubectl apply -f https://raw.githubusercontent.com/Kuadrant/authorino-operator/main/config/deploy/manifests.yaml +curl -sL https://raw.githubusercontent.com/Kuadrant/authorino-operator/main/utils/install.sh | bash -s ``` ## 2. Deploy the Talker API @@ -220,45 +220,42 @@ Create the AuthConfig: ```sh kubectl apply -f -<Authorino features in this guide: @@ -45,7 +45,7 @@ kind create cluster --name authorino-tutorial ## 1. Install the Authorino Operator ```sh -kubectl apply -f https://raw.githubusercontent.com/Kuadrant/authorino-operator/main/config/deploy/manifests.yaml +curl -sL https://raw.githubusercontent.com/Kuadrant/authorino-operator/main/utils/install.sh | bash -s ``` ## 2. Deploy the Talker API @@ -96,44 +96,45 @@ kubectl port-forward deployment/envoy 8000:8000 & ```sh kubectl apply -f -< Authorino features in this guide: @@ -34,7 +34,7 @@ kind create cluster --name authorino-tutorial ## 1. Install the Authorino Operator ```sh -kubectl apply -f https://raw.githubusercontent.com/Kuadrant/authorino-operator/main/config/deploy/manifests.yaml +curl -sL https://raw.githubusercontent.com/Kuadrant/authorino-operator/main/utils/install.sh | bash -s ``` ## 2. Deploy the Matrix Quotes web application @@ -85,41 +85,40 @@ kubectl port-forward deployment/envoy 8000:8000 & ```sh kubectl apply -f -< Authorino features in this guide: @@ -59,7 +59,7 @@ kubectl -n keycloak port-forward deployment/keycloak 8080:8080 & ## 1. Install the Authorino Operator ```sh -kubectl apply -f https://raw.githubusercontent.com/Kuadrant/authorino-operator/main/config/deploy/manifests.yaml +curl -sL https://raw.githubusercontent.com/Kuadrant/authorino-operator/main/utils/install.sh | bash -s ``` ## 2. Create the namespaces @@ -136,47 +136,45 @@ Create the config: ```sh kubectl -n edge apply -f -< Authorino features in this guide: @@ -46,7 +46,7 @@ kubectl -n keycloak apply -f https://raw.githubusercontent.com/kuadrant/authorin ## 1. Install the Authorino Operator ```sh -kubectl apply -f https://raw.githubusercontent.com/Kuadrant/authorino-operator/main/config/deploy/manifests.yaml +curl -sL https://raw.githubusercontent.com/Kuadrant/authorino-operator/main/utils/install.sh | bash -s ``` ## 2. Deploy the Talker API @@ -296,43 +296,42 @@ kubectl apply -f https://raw.githubusercontent.com/Kuadrant/authorino-examples/m ```sh kubectl apply -f -<Authorino features in this guide: @@ -36,7 +36,7 @@ kind create cluster --name authorino-tutorial ## 1. Install the Authorino Operator ```sh -kubectl apply -f https://raw.githubusercontent.com/Kuadrant/authorino-operator/main/config/deploy/manifests.yaml +curl -sL https://raw.githubusercontent.com/Kuadrant/authorino-operator/main/utils/install.sh | bash -s ``` ## 2. Deploy the Talker API @@ -91,45 +91,44 @@ The implementation relies on the [`X-Forwarded-For`](https://datatracker.ietf.or ```sh kubectl apply -f -< Authorino features in this guide: @@ -35,7 +35,7 @@ kind create cluster --name authorino-tutorial ## 1. Install the Authorino Operator ```sh -kubectl apply -f https://raw.githubusercontent.com/Kuadrant/authorino-operator/main/config/deploy/manifests.yaml +curl -sL https://raw.githubusercontent.com/Kuadrant/authorino-operator/main/utils/install.sh | bash -s ``` ## 2. Deploy the Talker API @@ -86,39 +86,39 @@ kubectl port-forward deployment/envoy 8000:8000 & ```sh kubectl apply -f -< Authorino features in this guide: @@ -33,7 +33,7 @@ kind create cluster --name authorino-tutorial ## 1. Install the Authorino Operator ```sh -kubectl apply -f https://raw.githubusercontent.com/Kuadrant/authorino-operator/main/config/deploy/manifests.yaml +curl -sL https://raw.githubusercontent.com/Kuadrant/authorino-operator/main/utils/install.sh | bash -s ``` ## 2. Deploy the Talker API @@ -89,38 +89,38 @@ The following defines a JSON object to be injected as an added HTTP header into ```sh kubectl apply -f -< Authorino features in this guide: @@ -48,7 +48,7 @@ kubectl -n keycloak port-forward deployment/keycloak 8080:8080 & ## 1. Install the Authorino Operator ```sh -kubectl apply -f https://raw.githubusercontent.com/Kuadrant/authorino-operator/main/config/deploy/manifests.yaml +curl -sL https://raw.githubusercontent.com/Kuadrant/authorino-operator/main/utils/install.sh | bash -s ``` ## 2. Deploy the Talker API @@ -105,32 +105,32 @@ The implementation relies on the [`X-Forwarded-For`](https://datatracker.ietf.or ```sh kubectl apply -f -< Authorino features in this guide: @@ -46,7 +46,7 @@ kubectl -n keycloak port-forward deployment/keycloak 8080:8080 & ## 1. Install the Authorino Operator ```sh -kubectl apply -f https://raw.githubusercontent.com/Kuadrant/authorino-operator/main/config/deploy/manifests.yaml +curl -sL https://raw.githubusercontent.com/Kuadrant/authorino-operator/main/utils/install.sh | bash -s ``` ## 2. Deploy the Talker API @@ -107,50 +107,52 @@ Whenever an RPT with proper permissions is obtained by Authorino, the RPT is sup ```sh kubectl apply -f -< Authorino features in this guide: @@ -35,7 +35,7 @@ kind create cluster --name authorino-tutorial ## 1. Install the Authorino Operator ```sh -kubectl apply -f https://raw.githubusercontent.com/Kuadrant/authorino-operator/main/config/deploy/manifests.yaml +curl -sL https://raw.githubusercontent.com/Kuadrant/authorino-operator/main/utils/install.sh | bash -s ``` ## 2. Deploy the Talker API @@ -88,7 +88,7 @@ The `AuthConfig` below sets all Kubernetes service accounts as trusted users of ```sh kubectl apply -f -< Authorino features in this guide: @@ -36,7 +36,7 @@ kind create cluster --name authorino-tutorial ## 1. Install the Authorino Operator ```sh -kubectl apply -f https://raw.githubusercontent.com/Kuadrant/authorino-operator/main/config/deploy/manifests.yaml +curl -sL https://raw.githubusercontent.com/Kuadrant/authorino-operator/main/utils/install.sh | bash -s ``` ## 2. Deploy the Talker API @@ -87,7 +87,7 @@ kubectl port-forward deployment/envoy 8000:8000 & ```sh kubectl apply -f -< Authorino features in this guide: @@ -23,7 +23,6 @@ Verify client X.509 certificates against trusted root CAs stored in Kubernetes ` ## Requirements - Kubernetes server -- [cert-manager](https://github.com/jetstack/cert-manager) Create a containerized Kubernetes server locally using [Kind](https://kind.sigs.k8s.io): @@ -31,18 +30,14 @@ Create a containerized Kubernetes server locally using [Kind](https://kind.sigs. kind create cluster --name authorino-tutorial ``` -Install cert-manager in the cluster: - -```sh -kubectl apply -f https://github.com/jetstack/cert-manager/releases/download/v1.4.0/cert-manager.yaml -``` - ## 1. Install the Authorino Operator ```sh -kubectl apply -f https://raw.githubusercontent.com/Kuadrant/authorino-operator/main/config/deploy/manifests.yaml +curl -sL https://raw.githubusercontent.com/Kuadrant/authorino-operator/main/utils/install.sh | bash -s ``` +This step will also install [cert-manager](https://github.com/jetstack/cert-manager) in the cluster (required). + ## 2. Deploy Authorino Create the TLS certificates for the Authorino service: @@ -292,26 +287,26 @@ kubectl port-forward deployment/envoy 8000:8000 & ```sh kubectl apply -f -< Authorino features in this guide: @@ -63,7 +63,7 @@ kubectl -n a12n-server port-forward deployment/a12n-server 8531:8531 & ## 1. Install the Authorino Operator ```sh -kubectl apply -f https://raw.githubusercontent.com/Kuadrant/authorino-operator/main/config/deploy/manifests.yaml +curl -sL https://raw.githubusercontent.com/Kuadrant/authorino-operator/main/utils/install.sh | bash -s ``` ## 2. Deploy the Talker API @@ -144,36 +144,36 @@ Create the config: ```sh kubectl apply -f -< Authorino features in this guide: @@ -47,7 +47,7 @@ kubectl -n keycloak port-forward deployment/keycloak 8080:8080 & ## 1. Install the Authorino Operator ```sh -kubectl apply -f https://raw.githubusercontent.com/Kuadrant/authorino-operator/main/config/deploy/manifests.yaml +curl -sL https://raw.githubusercontent.com/Kuadrant/authorino-operator/main/utils/install.sh | bash -s ``` ## 2. Deploy the Talker API @@ -98,17 +98,17 @@ kubectl port-forward deployment/envoy 8000:8000 & ```sh kubectl apply -f -< Authorino features in this guide: @@ -48,7 +48,7 @@ kubectl -n keycloak port-forward deployment/keycloak 8080:8080 & ## 1. Install the Authorino Operator ```sh -kubectl apply -f https://raw.githubusercontent.com/Kuadrant/authorino-operator/main/config/deploy/manifests.yaml +curl -sL https://raw.githubusercontent.com/Kuadrant/authorino-operator/main/utils/install.sh | bash -s ``` ## 2. Deploy the Talker API @@ -109,7 +109,7 @@ Apply the AuthConfig: ```sh kubectl apply -f -<Authorino features in this guide: @@ -49,7 +49,7 @@ kubectl -n keycloak port-forward deployment/keycloak 8080:8080 & ## 1. Install the Authorino Operator ```sh -kubectl apply -f https://raw.githubusercontent.com/Kuadrant/authorino-operator/main/config/deploy/manifests.yaml +curl -sL https://raw.githubusercontent.com/Kuadrant/authorino-operator/main/utils/install.sh | bash -s ``` ## 2. Deploy the Talker API @@ -100,28 +100,28 @@ kubectl port-forward deployment/envoy 8000:8000 & ```sh kubectl apply -f -<Authorino features in this guide: @@ -35,7 +35,7 @@ kind create cluster --name authorino-tutorial ## 1. Install the Authorino Operator ```sh -kubectl apply -f https://raw.githubusercontent.com/Kuadrant/authorino-operator/main/config/deploy/manifests.yaml +curl -sL https://raw.githubusercontent.com/Kuadrant/authorino-operator/main/utils/install.sh | bash -s ``` ## 2. Deploy the Talker API @@ -92,31 +92,31 @@ _Optional._ Set [`use_remote_address: true`](https://www.envoyproxy.io/docs/envo ```sh kubectl apply -f -< Authorino features in this guide:
    -
  • Identity verification & authentication → Auth credentials
  • -
  • Identity verification & authentication → API key
  • +
  • Identity verification & authentication → Auth credentials
  • +
  • Identity verification & authentication → API key
@@ -33,7 +33,7 @@ kind create cluster --name authorino-tutorial ## 1. Install the Authorino Operator ```sh -kubectl apply -f https://raw.githubusercontent.com/Kuadrant/authorino-operator/main/config/deploy/manifests.yaml +curl -sL https://raw.githubusercontent.com/Kuadrant/authorino-operator/main/utils/install.sh | bash -s ``` ## 2. Deploy the Talker API @@ -92,51 +92,51 @@ In this example, `member` users can authenticate supplying the API key in any of ```sh kubectl apply -f -<Authorino features in this guide: @@ -47,7 +47,7 @@ kubectl -n keycloak port-forward deployment/keycloak 8080:8080 & ## 1. Install the Authorino Operator ```sh -kubectl apply -f https://raw.githubusercontent.com/Kuadrant/authorino-operator/main/config/deploy/manifests.yaml +curl -sL https://raw.githubusercontent.com/Kuadrant/authorino-operator/main/utils/install.sh | bash -s ``` ## 2. Deploy the Talker API @@ -119,70 +119,63 @@ Create the config: ```sh kubectl apply -f -<Authorino features in this guide:
  • Sharding
  • -
  • Identity verification & authentication → API key
  • +
  • Identity verification & authentication → API key
@@ -35,7 +35,7 @@ kind create cluster --name authorino-tutorial ## 1. Install the Authorino Operator ```sh -kubectl apply -f https://raw.githubusercontent.com/Kuadrant/authorino-operator/main/config/deploy/manifests.yaml +curl -sL https://raw.githubusercontent.com/Kuadrant/authorino-operator/main/utils/install.sh | bash -s ``` ## 2. Deploy a couple instances of Authorino @@ -98,7 +98,7 @@ Create an `AuthConfig`: ```sh kubectl -n myapp apply -f -< Authorino features in this guide: @@ -48,7 +48,7 @@ kubectl -n keycloak apply -f https://raw.githubusercontent.com/kuadrant/authorin ## 1. Install the Authorino Operator ```sh -kubectl apply -f https://raw.githubusercontent.com/Kuadrant/authorino-operator/main/config/deploy/manifests.yaml +curl -sL https://raw.githubusercontent.com/Kuadrant/authorino-operator/main/utils/install.sh | bash -s ``` ## 2. Deploy the Talker API @@ -105,43 +105,42 @@ Without normalizing identity claims from these two different sources, the policy ```sh kubectl apply -f -< Authorino features in this guide: @@ -43,7 +43,6 @@ For convenience, the same instance of Authorino used to enforce the AuthConfig a ## Requirements - Kubernetes server -- [cert-manager](https://github.com/jetstack/cert-manager) - Auth server / Identity Provider (IdP) that implements OpenID Connect authentication and OpenID Connect Discovery (e.g. [Keycloak](https://www.keycloak.org)) Create a containerized Kubernetes server locally using [Kind](https://kind.sigs.k8s.io): @@ -52,12 +51,6 @@ Create a containerized Kubernetes server locally using [Kind](https://kind.sigs. kind create cluster --name authorino-tutorial ``` -Install cert-manager: - -```sh -kubectl apply -f https://github.com/jetstack/cert-manager/releases/download/v1.4.0/cert-manager.yaml -``` - Deploy a Keycloak server preloaded with all the realm settings required for this guide: ```sh @@ -68,9 +61,11 @@ kubectl -n keycloak apply -f https://raw.githubusercontent.com/kuadrant/authorin ## 1. Install the Authorino Operator ```sh -kubectl apply -f https://raw.githubusercontent.com/Kuadrant/authorino-operator/main/config/deploy/manifests.yaml +curl -sL https://raw.githubusercontent.com/Kuadrant/authorino-operator/main/utils/install.sh | bash -s ``` +This step will also install [cert-manager](https://github.com/jetstack/cert-manager) in the cluster (required). + ## 2. Deploy Authorino Create the namespace: @@ -117,7 +112,7 @@ Create the `AuthConfig`: ```sh kubectl -n authorino apply -f -< 0; not forbidden } - allValues: true - - - name: apikey-authn-requires-k8s-role-binding - priority: 1 - when: - - selector: auth.authorization.features.apiKey - operator: eq - value: "true" - kubernetes: - user: - valueFrom: { authJSON: auth.identity.username } - resourceAttributes: - namespace: { value: authorino } - group: { value: authorino.kuadrant.io } - resource: { value: authconfigs-with-apikeys } - verb: { value: create } - - - name: metadata-cache-ttl - priority: 1 - opa: - inlineRego: | - invalid_ttl = input.auth.authorization.features.authconfig.spec.metadata[_].cache.ttl != 300 - allow { not invalid_ttl } + "features": + opa: + rego: | + authconfig = json.unmarshal(input.context.request.http.body).request.object + + forbidden { count(object.get(authconfig.spec, "authentication", [])) == 0 } + forbidden { authconfig.spec.authentication[_].anonymous } + forbidden { authconfig.spec.authentication[_].kubernetesTokenReview } + forbidden { authconfig.spec.authentication[_].plain } + forbidden { authconfig.spec.authorization[_].kubernetesSubjectAccessReview } + forbidden { authconfig.spec.response.success.headers[_].wristband } + + apiKey { authconfig.spec.authentication[_].apiKey } + + allow { count(authconfig.spec.authentication) > 0; not forbidden } + allValues: true + + "apikey-authn-requires-k8s-role-binding": + priority: 1 + when: + - selector: auth.authorization.features.apiKey + operator: eq + value: "true" + kubernetesSubjectAccessReview: + user: + selector: auth.identity.username + resourceAttributes: + namespace: { value: authorino } + group: { value: authorino.kuadrant.io } + resource: { value: authconfigs-with-apikeys } + verb: { value: create } + + "metadata-cache-ttl": + priority: 1 + opa: + rego: | + invalid_ttl = input.auth.authorization.features.authconfig.spec.metadata[_].cache.ttl != 300 + allow { not invalid_ttl } EOF ``` @@ -216,7 +211,7 @@ webhooks: path: /check rules: - apiGroups: ["authorino.kuadrant.io"] - apiVersions: ["v1beta1"] + apiVersions: ["v1beta2"] resources: ["authconfigs"] operations: ["CREATE", "UPDATE"] scope: Namespaced @@ -237,17 +232,17 @@ kubectl create namespace myapp ```sh kubectl -n myapp apply -f -<://:/, + where = /://:/, + where = /://:/, + where = /://:/, + where = /