diff --git a/Makefile b/Makefile index 809ab4cc42a..91082ce9841 100644 --- a/Makefile +++ b/Makefile @@ -280,7 +280,8 @@ ifdef SUITES SUITES_ARG = --suites $(SUITES) COMPLETE_SUITES_ARG = -args $(SUITES_ARG) endif -TEST_FEATURE_GATES ?= WorkspaceMounts=true,CacheAPIs=true +TEST_FEATURE_GATES ?= WorkspaceMounts=true,CacheAPIs=true,WorkspaceAuthentication=true +PROXY_FEATURE_GATES ?= $(TEST_FEATURE_GATES) .PHONY: test-e2e ifdef USE_GOTESTSUM @@ -337,7 +338,7 @@ endif test-e2e-sharded-minimal: build-all mkdir -p "$(LOG_DIR)" "$(WORK_DIR)/.kcp" rm -f "$(WORK_DIR)/.kcp/ready-to-test" - UNSAFE_E2E_HACK_DISABLE_ETCD_FSYNC=true NO_GORUN=1 ./bin/sharded-test-server --quiet --v=2 --log-dir-path="$(LOG_DIR)" --work-dir-path="$(WORK_DIR)" --shard-run-virtual-workspaces=false --shard-feature-gates=$(TEST_FEATURE_GATES) $(TEST_SERVER_ARGS) --number-of-shards=$(SHARDS) 2>&1 & PID=$$!; echo "PID $$PID" && \ + UNSAFE_E2E_HACK_DISABLE_ETCD_FSYNC=true NO_GORUN=1 ./bin/sharded-test-server --quiet --v=2 --log-dir-path="$(LOG_DIR)" --work-dir-path="$(WORK_DIR)" --shard-run-virtual-workspaces=false --shard-feature-gates=$(TEST_FEATURE_GATES) --proxy-feature-gates=$(PROXY_FEATURE_GATES) $(TEST_SERVER_ARGS) --number-of-shards=$(SHARDS) 2>&1 & PID=$$!; echo "PID $$PID" && \ trap 'kill -TERM $$PID && wait $$PID' TERM INT EXIT && \ while [ ! -f "$(WORK_DIR)/.kcp/ready-to-test" ]; do sleep 1; done && \ echo 'Starting test(s)' && \ @@ -354,7 +355,7 @@ test-run-sharded-server: LOG_DIR ?= $(WORK_DIR)/.kcp test-run-sharded-server: mkdir -p "$(LOG_DIR)" "$(WORK_DIR)/.kcp" rm -f "$(WORK_DIR)/.kcp/ready-to-test" - UNSAFE_E2E_HACK_DISABLE_ETCD_FSYNC=true NO_GORUN=1 ./bin/sharded-test-server --quiet --v=2 --log-dir-path="$(LOG_DIR)" --work-dir-path="$(WORK_DIR)" --shard-run-virtual-workspaces=false --shard-feature-gates=$(TEST_FEATURE_GATES) $(TEST_SERVER_ARGS) --number-of-shards=2 2>&1 & PID=$$!; echo "PID $$PID" && \ + UNSAFE_E2E_HACK_DISABLE_ETCD_FSYNC=true NO_GORUN=1 ./bin/sharded-test-server --quiet --v=2 --log-dir-path="$(LOG_DIR)" --work-dir-path="$(WORK_DIR)" --shard-run-virtual-workspaces=false --shard-feature-gates=$(TEST_FEATURE_GATES) --proxy-feature-gates=$(PROXY_FEATURE_GATES) $(TEST_SERVER_ARGS) --number-of-shards=2 2>&1 & PID=$$!; echo "PID $$PID" && \ trap 'kill -TERM $$PID && wait $$PID' TERM INT EXIT && \ while [ ! -f "$(WORK_DIR)/.kcp/ready-to-test" ]; do sleep 1; done && \ echo 'Server started' && \ diff --git a/cmd/kcp-front-proxy/options/options.go b/cmd/kcp-front-proxy/options/options.go index 2d6f605f579..3881e197e40 100644 --- a/cmd/kcp-front-proxy/options/options.go +++ b/cmd/kcp-front-proxy/options/options.go @@ -17,10 +17,13 @@ limitations under the License. package options import ( + "strings" + cliflag "k8s.io/component-base/cli/flag" "k8s.io/component-base/logs" logsapiv1 "k8s.io/component-base/logs/api/v1" + kcpfeatures "github.com/kcp-dev/kcp/pkg/features" proxyoptions "github.com/kcp-dev/kcp/pkg/proxy/options" ) @@ -43,6 +46,11 @@ func NewOptions() *Options { func (o *Options) AddFlags(fss *cliflag.NamedFlagSets) { o.Proxy.AddFlags(fss) logsapiv1.AddFlags(o.Logs, fss.FlagSet("logging")) + + // add flags that are filtered out from upstream, but overridden here with our own version + fss.FlagSet("KCP").Var(kcpfeatures.NewFlagValue(), "feature-gates", ""+ + "A set of key=value pairs that describe feature gates for alpha/experimental features. "+ + "Options are:\n"+strings.Join(kcpfeatures.KnownFeatures(), "\n")) // hide kube-only gates } func (o *Options) Complete() error { diff --git a/config/crds/tenancy.kcp.io_workspaceauthenticationconfigurations.yaml b/config/crds/tenancy.kcp.io_workspaceauthenticationconfigurations.yaml new file mode 100644 index 00000000000..717d449082b --- /dev/null +++ b/config/crds/tenancy.kcp.io_workspaceauthenticationconfigurations.yaml @@ -0,0 +1,208 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.17.3 + name: workspaceauthenticationconfigurations.tenancy.kcp.io +spec: + group: tenancy.kcp.io + names: + categories: + - kcp + kind: WorkspaceAuthenticationConfiguration + listKind: WorkspaceAuthenticationConfigurationList + plural: workspaceauthenticationconfigurations + singular: workspaceauthenticationconfiguration + scope: Cluster + versions: + - name: v1alpha1 + schema: + openAPIV3Schema: + description: |- + WorkspaceAuthenticationConfiguration specifies additional authentication options + for workspaces. + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + properties: + jwt: + items: + properties: + claimMappings: + description: ClaimMappings provides the configuration for claim + mapping. + properties: + extra: + items: + description: ExtraMapping provides the configuration for + a single extra mapping. + properties: + key: + type: string + valueExpression: + type: string + required: + - key + - valueExpression + type: object + type: array + groups: + description: PrefixedClaimOrExpression provides the configuration + for a single prefixed claim or expression. + properties: + claim: + type: string + expression: + type: string + prefix: + type: string + required: + - claim + type: object + uid: + description: ClaimOrExpression provides the configuration + for a single claim or expression. + properties: + claim: + type: string + expression: + type: string + required: + - claim + type: object + username: + description: PrefixedClaimOrExpression provides the configuration + for a single prefixed claim or expression. + properties: + claim: + type: string + expression: + type: string + prefix: + type: string + required: + - claim + type: object + required: + - groups + - username + type: object + claimValidationRules: + items: + description: ClaimValidationRule provides the configuration + for a single claim validation rule. + properties: + claim: + type: string + expression: + type: string + message: + type: string + requiredValue: + type: string + required: + - claim + - expression + - message + - requiredValue + type: object + type: array + issuer: + description: Issuer provides the configuration for an external + provider's specific settings. + properties: + audienceMatchPolicy: + description: AudienceMatchPolicyType is a set of valid values + for Issuer.AudienceMatchPolicy. + type: string + audiences: + items: + type: string + type: array + certificateAuthority: + type: string + discoveryURL: + description: |- + discoveryURL, if specified, overrides the URL used to fetch discovery + information instead of using "{url}/.well-known/openid-configuration". + The exact value specified is used, so "/.well-known/openid-configuration" + must be included in discoveryURL if needed. + + The "issuer" field in the fetched discovery information must match the "issuer.url" field + in the AuthenticationConfiguration and will be used to validate the "iss" claim in the presented JWT. + This is for scenarios where the well-known and jwks endpoints are hosted at a different + location than the issuer (such as locally in the cluster). + + Example: + A discovery url that is exposed using kubernetes service 'oidc' in namespace 'oidc-namespace' + and discovery information is available at '/.well-known/openid-configuration'. + discoveryURL: "https://oidc.oidc-namespace/.well-known/openid-configuration" + certificateAuthority is used to verify the TLS connection and the hostname on the leaf certificate + must be set to 'oidc.oidc-namespace'. + + curl https://oidc.oidc-namespace/.well-known/openid-configuration (.discoveryURL field) + { + issuer: "https://oidc.example.com" (.url field) + } + + discoveryURL must be different from url. + Required to be unique across all JWT authenticators. + Note that egress selection configuration is not used for this network connection. + type: string + url: + description: |- + url points to the issuer URL in a format https://url or https://url/path. + This must match the "iss" claim in the presented JWT, and the issuer returned from discovery. + Same value as the --oidc-issuer-url flag. + Discovery information is fetched from "{url}/.well-known/openid-configuration" unless overridden by discoveryURL. + Required to be unique across all JWT authenticators. + Note that egress selection configuration is not used for this network connection. + type: string + required: + - url + type: object + userValidationRules: + items: + description: UserValidationRule provides the configuration + for a single user validation rule. + properties: + expression: + type: string + message: + type: string + required: + - expression + - message + type: object + type: array + required: + - claimMappings + - issuer + type: object + type: array + required: + - jwt + type: object + required: + - metadata + - spec + type: object + served: true + storage: true diff --git a/config/crds/tenancy.kcp.io_workspacetypes.yaml b/config/crds/tenancy.kcp.io_workspacetypes.yaml index 8901801eb58..7ab29cb9957 100644 --- a/config/crds/tenancy.kcp.io_workspacetypes.yaml +++ b/config/crds/tenancy.kcp.io_workspacetypes.yaml @@ -56,6 +56,21 @@ spec: additionalWorkspaceLabels are a set of labels that will be added to a Workspace on creation. type: object + authenticationConfigurations: + description: |- + authenticationConfigurations are additional authentication options that should apply to any + workspace using this workspace type. + items: + description: AuthenticationConfigurationReference provides the fields + necessary to resolve a WorkspaceAuthenticationConfiguration. + properties: + name: + description: name is the name of the WorkspaceAuthenticationConfiguration. + type: string + required: + - name + type: object + type: array defaultAPIBindingLifecycle: description: Configure the lifecycle behaviour of defaultAPIBindings. enum: diff --git a/config/root-phase0/apiexport-tenancy.kcp.io.yaml b/config/root-phase0/apiexport-tenancy.kcp.io.yaml index bffaf706de9..5a3311e42ac 100644 --- a/config/root-phase0/apiexport-tenancy.kcp.io.yaml +++ b/config/root-phase0/apiexport-tenancy.kcp.io.yaml @@ -7,6 +7,11 @@ spec: maximalPermissionPolicy: local: {} resources: + - group: tenancy.kcp.io + name: workspaceauthenticationconfigurations + schema: v250802-1b3cd3d0d.workspaceauthenticationconfigurations.tenancy.kcp.io + storage: + crd: {} - group: tenancy.kcp.io name: workspaces schema: v250421-25d98218b.workspaces.tenancy.kcp.io @@ -14,7 +19,7 @@ spec: crd: {} - group: tenancy.kcp.io name: workspacetypes - schema: v250603-d4d365c8e.workspacetypes.tenancy.kcp.io + schema: v250806-4c99c4583.workspacetypes.tenancy.kcp.io storage: crd: {} status: {} diff --git a/config/root-phase0/apiresourceschema-workspaceauthenticationconfigurations.tenancy.kcp.io.yaml b/config/root-phase0/apiresourceschema-workspaceauthenticationconfigurations.tenancy.kcp.io.yaml new file mode 100644 index 00000000000..aad30732901 --- /dev/null +++ b/config/root-phase0/apiresourceschema-workspaceauthenticationconfigurations.tenancy.kcp.io.yaml @@ -0,0 +1,206 @@ +apiVersion: apis.kcp.io/v1alpha1 +kind: APIResourceSchema +metadata: + creationTimestamp: null + name: v250802-1b3cd3d0d.workspaceauthenticationconfigurations.tenancy.kcp.io +spec: + group: tenancy.kcp.io + names: + categories: + - kcp + kind: WorkspaceAuthenticationConfiguration + listKind: WorkspaceAuthenticationConfigurationList + plural: workspaceauthenticationconfigurations + singular: workspaceauthenticationconfiguration + scope: Cluster + versions: + - name: v1alpha1 + schema: + description: |- + WorkspaceAuthenticationConfiguration specifies additional authentication options + for workspaces. + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + properties: + jwt: + items: + properties: + claimMappings: + description: ClaimMappings provides the configuration for claim + mapping. + properties: + extra: + items: + description: ExtraMapping provides the configuration for + a single extra mapping. + properties: + key: + type: string + valueExpression: + type: string + required: + - key + - valueExpression + type: object + type: array + groups: + description: PrefixedClaimOrExpression provides the configuration + for a single prefixed claim or expression. + properties: + claim: + type: string + expression: + type: string + prefix: + type: string + required: + - claim + type: object + uid: + description: ClaimOrExpression provides the configuration + for a single claim or expression. + properties: + claim: + type: string + expression: + type: string + required: + - claim + type: object + username: + description: PrefixedClaimOrExpression provides the configuration + for a single prefixed claim or expression. + properties: + claim: + type: string + expression: + type: string + prefix: + type: string + required: + - claim + type: object + required: + - groups + - username + type: object + claimValidationRules: + items: + description: ClaimValidationRule provides the configuration + for a single claim validation rule. + properties: + claim: + type: string + expression: + type: string + message: + type: string + requiredValue: + type: string + required: + - claim + - expression + - message + - requiredValue + type: object + type: array + issuer: + description: Issuer provides the configuration for an external + provider's specific settings. + properties: + audienceMatchPolicy: + description: AudienceMatchPolicyType is a set of valid values + for Issuer.AudienceMatchPolicy. + type: string + audiences: + items: + type: string + type: array + certificateAuthority: + type: string + discoveryURL: + description: |- + discoveryURL, if specified, overrides the URL used to fetch discovery + information instead of using "{url}/.well-known/openid-configuration". + The exact value specified is used, so "/.well-known/openid-configuration" + must be included in discoveryURL if needed. + + The "issuer" field in the fetched discovery information must match the "issuer.url" field + in the AuthenticationConfiguration and will be used to validate the "iss" claim in the presented JWT. + This is for scenarios where the well-known and jwks endpoints are hosted at a different + location than the issuer (such as locally in the cluster). + + Example: + A discovery url that is exposed using kubernetes service 'oidc' in namespace 'oidc-namespace' + and discovery information is available at '/.well-known/openid-configuration'. + discoveryURL: "https://oidc.oidc-namespace/.well-known/openid-configuration" + certificateAuthority is used to verify the TLS connection and the hostname on the leaf certificate + must be set to 'oidc.oidc-namespace'. + + curl https://oidc.oidc-namespace/.well-known/openid-configuration (.discoveryURL field) + { + issuer: "https://oidc.example.com" (.url field) + } + + discoveryURL must be different from url. + Required to be unique across all JWT authenticators. + Note that egress selection configuration is not used for this network connection. + type: string + url: + description: |- + url points to the issuer URL in a format https://url or https://url/path. + This must match the "iss" claim in the presented JWT, and the issuer returned from discovery. + Same value as the --oidc-issuer-url flag. + Discovery information is fetched from "{url}/.well-known/openid-configuration" unless overridden by discoveryURL. + Required to be unique across all JWT authenticators. + Note that egress selection configuration is not used for this network connection. + type: string + required: + - url + type: object + userValidationRules: + items: + description: UserValidationRule provides the configuration for + a single user validation rule. + properties: + expression: + type: string + message: + type: string + required: + - expression + - message + type: object + type: array + required: + - claimMappings + - issuer + type: object + type: array + required: + - jwt + type: object + required: + - metadata + - spec + type: object + served: true + storage: true + subresources: {} diff --git a/config/root-phase0/apiresourceschema-workspacetypes.tenancy.kcp.io.yaml b/config/root-phase0/apiresourceschema-workspacetypes.tenancy.kcp.io.yaml index 9feba7b11eb..4d3f37d9dca 100644 --- a/config/root-phase0/apiresourceschema-workspacetypes.tenancy.kcp.io.yaml +++ b/config/root-phase0/apiresourceschema-workspacetypes.tenancy.kcp.io.yaml @@ -2,7 +2,7 @@ apiVersion: apis.kcp.io/v1alpha1 kind: APIResourceSchema metadata: creationTimestamp: null - name: v250603-d4d365c8e.workspacetypes.tenancy.kcp.io + name: v250806-4c99c4583.workspacetypes.tenancy.kcp.io spec: group: tenancy.kcp.io names: @@ -54,6 +54,21 @@ spec: additionalWorkspaceLabels are a set of labels that will be added to a Workspace on creation. type: object + authenticationConfigurations: + description: |- + authenticationConfigurations are additional authentication options that should apply to any + workspace using this workspace type. + items: + description: AuthenticationConfigurationReference provides the fields + necessary to resolve a WorkspaceAuthenticationConfiguration. + properties: + name: + description: name is the name of the WorkspaceAuthenticationConfiguration. + type: string + required: + - name + type: object + type: array defaultAPIBindingLifecycle: description: Configure the lifecycle behaviour of defaultAPIBindings. enum: diff --git a/docs/content/concepts/authentication/oidc.md b/docs/content/concepts/authentication/oidc.md index b18e92f2ebd..0bfe4ef47a4 100644 --- a/docs/content/concepts/authentication/oidc.md +++ b/docs/content/concepts/authentication/oidc.md @@ -8,170 +8,164 @@ description: > OpenID Connect (OIDC) is a simple identity layer on top of the OAuth 2.0 protocol, which allows clients to verify the identity of users based on the authentication performed by an external authorization server. In this guide, we will set up OIDC authentication in kcp using Dex as the identity provider. For more details on Kubernetes specific configuration, please refer to this [page](https://kubernetes.io/docs/reference/access-authn-authz/authentication/#openid-connect-tokens). - ## Configure kcp OIDC Authentication Using OIDC Flags kcp server will configure the OIDC authentication using the same Kubernetes control plane settings, to start kcp with OIDC authentication enabled, you can simply pass OIDC flags during the start of the kcp server: ```bash kcp start \ ---oidc-issuer-url= \ ---oidc-client-id=\ ---oidc-groups-claim= \ ---oidc-ca-file= + --oidc-issuer-url "" \ + --oidc-client-id "" \ + --oidc-groups-claim "" \ + --oidc-ca-file "" ``` -- `--oidc-issuer-url` URL of the provider that allows the API server to discover public signing keys. - -- `--oidc-client-id` A client id that all tokens must be issued for. - -- `--oidc-groups-claim` JWT claim to use as the user's group. - -- `--oidc-ca-file` The path to the certificate for the CA that signed your identity provider's web certificate. +- `--oidc-issuer-url` – URL of the provider that allows the API server to discover public signing keys. +- `--oidc-client-id` – A client ID that all tokens must be issued for. +- `--oidc-groups-claim` – JWT claim to use as the user's group. +- `--oidc-ca-file` – The path to the certificate for the CA that signed your identity provider's web certificate. You can also set: -- `--oidc-username-claim` JWT claim to use as the user name. - -- `--oidc-required-claim` A key=value pair that describes a required claim in the ID Token. - -- `--oidc-signing-algs` The signing algorithms accepted. - -- `--oidc-username-prefix` Prefix prepended to username claims to prevent clashes with existing names. - -- `--oidc-groups-prefix` Prefix prepended to group claims to prevent clashes with existing names +- `--oidc-username-claim` – JWT claim to use as the user name. +- `--oidc-required-claim` – A key=value pair that describes a required claim in the ID Token. +- `--oidc-signing-algs` – The signing algorithms accepted. +- `--oidc-username-prefix` – Prefix prepended to username claims to prevent clashes with existing names. +- `--oidc-groups-prefix` – Prefix prepended to group claims to prevent clashes with existing names ## Configure kcp OIDC Authentication Using Structured Authentication Configuration Alternatively, you can use the beta feature of authentication configuration from a file and set up the kcp server with it. Please note that if you specify `--authentication-config` along with any of the `--oidc-*` command line arguments, this will be treated as a misconfiguration. -```bash +```yaml apiVersion: apiserver.config.k8s.io/v1beta1 kind: AuthenticationConfiguration # list of authenticators to authenticate Kubernetes users using JWT compliant tokens. # the maximum number of allowed authenticators is 64. jwt: -- issuer: - # url must be unique across all authenticators. - # url must not conflict with issuer configured in --service-account-issuer. - url: https://example.com # Same as --oidc-issuer-url. - # discoveryURL, if specified, overrides the URL used to fetch discovery - # information instead of using "{url}/.well-known/openid-configuration". - # The exact value specified is used, so "/.well-known/openid-configuration" - # must be included in discoveryURL if needed. - # - # The "issuer" field in the fetched discovery information must match the "issuer.url" field - # in the AuthenticationConfiguration and will be used to validate the "iss" claim in the presented JWT. - # This is for scenarios where the well-known and jwks endpoints are hosted at a different - # location than the issuer (such as locally in the cluster). - # discoveryURL must be different from url if specified and must be unique across all authenticators. - discoveryURL: https://discovery.example.com/.well-known/openid-configuration - # PEM encoded CA certificates used to validate the connection when fetching - # discovery information. If not set, the system verifier will be used. - # Same value as the content of the file referenced by the --oidc-ca-file flag. - certificateAuthority: - # audiences is the set of acceptable audiences the JWT must be issued to. - # At least one of the entries must match the "aud" claim in presented JWTs. - audiences: - - my-app # Same as --oidc-client-id. - - my-other-app - # this is required to be set to "MatchAny" when multiple audiences are specified. - audienceMatchPolicy: MatchAny - # rules applied to validate token claims to authenticate users. - claimValidationRules: - # Same as --oidc-required-claim key=value. - - claim: hd - requiredValue: example.com - # Instead of claim and requiredValue, you can use expression to validate the claim. - # expression is a CEL expression that evaluates to a boolean. - # all the expressions must evaluate to true for validation to succeed. - - expression: 'claims.hd == "example.com"' - # Message customizes the error message seen in the API server logs when the validation fails. - message: the hd claim must be set to example.com - - expression: 'claims.exp - claims.nbf <= 86400' - message: total token lifetime must not exceed 24 hours - claimMappings: - # username represents an option for the username attribute. - # This is the only required attribute. - username: - # Same as --oidc-username-claim. Mutually exclusive with username.expression. - claim: "sub" - # Same as --oidc-username-prefix. Mutually exclusive with username.expression. - # if username.claim is set, username.prefix is required. - # Explicitly set it to "" if no prefix is desired. - prefix: "" - # Mutually exclusive with username.claim and username.prefix. - # expression is a CEL expression that evaluates to a string. + - issuer: + # url must be unique across all authenticators. + # url must not conflict with issuer configured in --service-account-issuer. + url: https://example.com # Same as --oidc-issuer-url. + # discoveryURL, if specified, overrides the URL used to fetch discovery + # information instead of using "{url}/.well-known/openid-configuration". + # The exact value specified is used, so "/.well-known/openid-configuration" + # must be included in discoveryURL if needed. # - # 1. If username.expression uses 'claims.email', then 'claims.email_verified' must be used in - # username.expression or extra[*].valueExpression or claimValidationRules[*].expression. - # An example claim validation rule expression that matches the validation automatically - # applied when username.claim is set to 'email' is 'claims.?email_verified.orValue(true) == true'. - # By explicitly comparing the value to true, we let type-checking see the result will be a boolean, and - # to make sure a non-boolean email_verified claim will be caught at runtime. - # 2. If the username asserted based on username.expression is the empty string, the authentication - # request will fail. - expression: 'claims.username + ":external-user"' - # groups represents an option for the groups attribute. - groups: - # Same as --oidc-groups-claim. Mutually exclusive with groups.expression. - claim: "sub" - # Same as --oidc-groups-prefix. Mutually exclusive with groups.expression. - # if groups.claim is set, groups.prefix is required. - # Explicitly set it to "" if no prefix is desired. - prefix: "" - # Mutually exclusive with groups.claim and groups.prefix. - # expression is a CEL expression that evaluates to a string or a list of strings. - expression: 'claims.roles.split(",")' - # uid represents an option for the uid attribute. - uid: - # Mutually exclusive with uid.expression. - claim: 'sub' - # Mutually exclusive with uid.claim - # expression is a CEL expression that evaluates to a string. - expression: 'claims.sub' - # extra attributes to be added to the UserInfo object. Keys must be domain-prefix path and must be unique. - extra: - # key is a string to use as the extra attribute key. - # key must be a domain-prefix path (e.g. example.org/foo). All characters before the first "/" must be a valid - # subdomain as defined by RFC 1123. All characters trailing the first "/" must - # be valid HTTP Path characters as defined by RFC 3986. - # k8s.io, kubernetes.io and their subdomains are reserved for Kubernetes use and cannot be used. - # key must be lowercase and unique across all extra attributes. - - key: 'example.com/tenant' - # valueExpression is a CEL expression that evaluates to a string or a list of strings. - valueExpression: 'claims.tenant' - # validation rules applied to the final user object. - userValidationRules: - # expression is a CEL expression that evaluates to a boolean. - # all the expressions must evaluate to true for the user to be valid. - - expression: "!user.username.startsWith('system:')" - # Message customizes the error message seen in the API server logs when the validation fails. - message: 'username cannot used reserved system: prefix' - - expression: "user.groups.all(group, !group.startsWith('system:'))" - message: 'groups cannot used reserved system: prefix' + # The "issuer" field in the fetched discovery information must match the "issuer.url" field + # in the AuthenticationConfiguration and will be used to validate the "iss" claim in the presented JWT. + # This is for scenarios where the well-known and jwks endpoints are hosted at a different + # location than the issuer (such as locally in the cluster). + # discoveryURL must be different from url if specified and must be unique across all authenticators. + discoveryURL: https://discovery.example.com/.well-known/openid-configuration + # PEM encoded CA certificates used to validate the connection when fetching + # discovery information. If not set, the system verifier will be used. + # Same value as the content of the file referenced by the --oidc-ca-file flag. + certificateAuthority: + # audiences is the set of acceptable audiences the JWT must be issued to. + # At least one of the entries must match the "aud" claim in presented JWTs. + # Note that when per-workspace authentication is used, every token admitted into a workspace + # must also match these audiences, regardless of the workspace's configuration. + audiences: + - my-app # Same as --oidc-client-id. + - my-other-app + # this is required to be set to "MatchAny" when multiple audiences are specified. + audienceMatchPolicy: MatchAny + # rules applied to validate token claims to authenticate users. + claimValidationRules: + # Same as --oidc-required-claim key=value. + - claim: hd + requiredValue: example.com + # Instead of claim and requiredValue, you can use expression to validate the claim. + # expression is a CEL expression that evaluates to a boolean. + # all the expressions must evaluate to true for validation to succeed. + - expression: 'claims.hd == "example.com"' + # Message customizes the error message seen in the API server logs when the validation fails. + message: the hd claim must be set to example.com + - expression: 'claims.exp - claims.nbf <= 86400' + message: total token lifetime must not exceed 24 hours + claimMappings: + # username represents an option for the username attribute. + # This is the only required attribute. + username: + # Same as --oidc-username-claim. Mutually exclusive with username.expression. + claim: "sub" + # Same as --oidc-username-prefix. Mutually exclusive with username.expression. + # if username.claim is set, username.prefix is required. + # Explicitly set it to "" if no prefix is desired. + prefix: "" + # Mutually exclusive with username.claim and username.prefix. + # expression is a CEL expression that evaluates to a string. + # + # 1. If username.expression uses 'claims.email', then 'claims.email_verified' must be used in + # username.expression or extra[*].valueExpression or claimValidationRules[*].expression. + # An example claim validation rule expression that matches the validation automatically + # applied when username.claim is set to 'email' is 'claims.?email_verified.orValue(true) == true'. + # By explicitly comparing the value to true, we let type-checking see the result will be a boolean, and + # to make sure a non-boolean email_verified claim will be caught at runtime. + # 2. If the username asserted based on username.expression is the empty string, the authentication + # request will fail. + expression: 'claims.username + ":external-user"' + # groups represents an option for the groups attribute. + groups: + # Same as --oidc-groups-claim. Mutually exclusive with groups.expression. + claim: "sub" + # Same as --oidc-groups-prefix. Mutually exclusive with groups.expression. + # if groups.claim is set, groups.prefix is required. + # Explicitly set it to "" if no prefix is desired. + prefix: "" + # Mutually exclusive with groups.claim and groups.prefix. + # expression is a CEL expression that evaluates to a string or a list of strings. + expression: 'claims.roles.split(",")' + # uid represents an option for the uid attribute. + uid: + # Mutually exclusive with uid.expression. + claim: 'sub' + # Mutually exclusive with uid.claim + # expression is a CEL expression that evaluates to a string. + expression: 'claims.sub' + # extra attributes to be added to the UserInfo object. Keys must be domain-prefix path and must be unique. + extra: + # key is a string to use as the extra attribute key. + # key must be a domain-prefix path (e.g. example.org/foo). All characters before the first "/" must be a valid + # subdomain as defined by RFC 1123. All characters trailing the first "/" must + # be valid HTTP Path characters as defined by RFC 3986. + # k8s.io, kubernetes.io and their subdomains are reserved for Kubernetes use and cannot be used. + # key must be lowercase and unique across all extra attributes. + - key: 'example.com/tenant' + # valueExpression is a CEL expression that evaluates to a string or a list of strings. + valueExpression: 'claims.tenant' + # validation rules applied to the final user object. + userValidationRules: + # expression is a CEL expression that evaluates to a boolean. + # all the expressions must evaluate to true for the user to be valid. + - expression: "!user.username.startsWith('system:')" + # Message customizes the error message seen in the API server logs when the validation fails. + message: 'username cannot used reserved system: prefix' + - expression: "user.groups.all(group, !group.startsWith('system:'))" + message: 'groups cannot used reserved system: prefix' ``` To set up the AuthenticationConfiguration, similarly to the previous example with OIDC flags(`--oidc-issuer-url`, `--oidc-client-id`, `--oidc-groups-claim`, `--oidc-ca-file`), you can set it in the file: -```bash +```yaml apiVersion: apiserver.config.k8s.io/v1beta1 kind: AuthenticationConfiguration jwt: -- issuer: - url: - certificateAuthority: | - - audiences: - - - audienceMatchPolicy: MatchAny - claimMappings: - groups: - claim: - prefix: "" - claimValidationRules: [] - userValidationRules: [] + - issuer: + url: + certificateAuthority: | + + audiences: + - + audienceMatchPolicy: MatchAny + claimMappings: + groups: + claim: + prefix: "" + claimValidationRules: [] + userValidationRules: [] ``` To start the kcp server with the specified OIDC authentication configuration, pass the file path to the `--authentication-config` flag. diff --git a/docs/content/concepts/authentication/workspace.md b/docs/content/concepts/authentication/workspace.md new file mode 100644 index 00000000000..e2f70409aec --- /dev/null +++ b/docs/content/concepts/authentication/workspace.md @@ -0,0 +1,143 @@ +--- +description: > + How to admit users into workspaces by using custom JWT validators. +--- + +# Per-Workspace Authentication + +kcp supports a range of authentication options, but all of them are global and applicable to every workspace in a kcp system. However when integrating with external partners and services, it can be beneficial to be able to admit users into a workspace that do not necessarily have access to kcp as a whole. + +To enable this, kcp supports per-workspace authentication. In this model, a `WorkspaceType` configures a set of additional OIDC validators that are then used by kcp in addition to the global authentication mechanisms configured with CLI flags. Every workspace using these custom workspace types will then have these additional auth methods available. + +This document describes how to enable and use this feature. Please refer to [OIDC Configuration](./oidc.md) for more information about the global OIDC configuration. + +## Feature Gate + +The feature is guarded by a feature gate called `WorkspaceAuthentication`, which is disabled by default. It can be independently enabled on any front-proxy and/or any kcp shard servers, though it is recommended and intended to enable it on front-proxies only. The shard support is mainly for testing and developing. + +Add `--feature-gates=WorkspaceAuthentication=true` to the CLI flags on the front-proxy to enable the feature. + +## Overview + +Extra authentication for a workspace is configured using `WorkspaceAuthenticationConfiguration` (colloquially called "auth configs") objects, which can be thought of as CRD variants of the Kubernetes authentication configuration (as described in [OIDC Configuration](./oidc.md)). Each auth config contains a set of JWT validators that are capable of validating an incoming JWT bearer token. + +Workspace types then reference a set of auth configs, and their configuration will apply to all their workspaces/logicalclusters. Compared to many other settings in a `WorkspaceType` that work only as a preset for _new_ workspaces, the configured auth configs will continue to affect workspaces, so when a `WorkspaceType` is changed, this will impact existing workspaces, too. + +For every incoming HTTPS request, kcp will then resolve the logicalcluster, determine the used workspace type, assemble the list of auth configs and create an authenticator suitable for exactly the one logicalcluster targeted by the request. This workspace authenticator is an *alternative* to kcp's regular authentication (i.e. it forms a union with it). + +This authentication can happen in the front-proxy or on each shard individually. However only the front-proxy has a global view across all shards and will be able to reliably resolve everything necessary. The shard-local per-workspace authentication really only works on a single +shard and requires that all of `Workspace`, `WorkspaceType`, auth configs and `LogicalClusters` are on the same local shard. Because of this, it's recommended to use the front-proxy to handle per-workspace authentication. + +## OIDC + +It is important to understand how the per-workspace authenticators interact with the global ones. Most importantly, how audiences are handled. + +kcp has a `--api-audiences` flag that configures the global JWT audience claim that every single JWT needs to contain in order for it to be admitted. These global audiences are also required when using per-workspace authentication. + +For example, suppose kcp is started with `--api-audiences=https://kcp.example.com` and there is a `WorkspaceAuthenticationConfiguration` that defines a JWT validator using the audience `https://corp.initech.com`. For a token to be admitted into a workspace that uses this auth config, the token will have to contain *both* audiences. This is to ensure the token is actually meant to be used in kcp, regardless of which audiences are then configured per workspace. + +## Limitations + +This feature has some small limitations that users should keep in mind: + +* As mentioned above, the JWT validation for a workspace is not 100% independent from the global kcp authentication: tokens will need to contain kcp's global API audience (configured with `--api-audiences`) and any audience configured in the auth configs. You cannot have a token not contain kcp's global audience. +* `WorkspaceAuthenticationConfiguration` objects must reside in the same logicalcluster as the `WorkspaceType`. +* Workspace authenticators are started asynchronously and it will take a couple of seconds for them to be ready. +* The workspace authentication in the localproxy, as part of a single shard server, only knows about the data on the local shard and cannot handle cross-shard authentication. Users are advised to use the front-proxy instead. +* Even when the feature is disabled on all shards and all front-proxies, the API (CRDs) are always available in kcp. Admins might uses RBAC or webhooks to prevent creating `WorkspaceAuthenticationConfiguration` objects if needed. + +## Example + +In this example we want to create a workspace where users with tokens from our local OIDC provider are admitted to. + +### Step 0: Enabling the Feature + +Add `--feature-gates=WorkspaceAuthentication=true` to the CLI flags on the front-proxy to enable the feature. When developing or just testing, you can also add the feature gate to the kcp process like + +```bash +kcp start --feature-gates=WorkspaceAuthentication=true +``` + +### Step 1: Auth Configs + +First we need to create a `WorkspaceAuthenticationConfiguration` object in kcp: + +```yaml +apiVersion: tenancy.kcp.io/v1alpha1 +kind: WorkspaceAuthenticationConfiguration +metadata: + name: my-auth-config +spec: + jwt: + - issuer: + url: + certificateAuthority: | + + audiences: + - + audienceMatchPolicy: MatchAny + claimMappings: + groups: + claim: + prefix: "" + claimValidationRules: [] + userValidationRules: [] +``` + +Conveniently, this CRD has the exact same structure as Kubernetes' own `AuthenticationConfiguration`. + +### Step 2: Workspace Type + +Next we need to have a workspace type that uses our new auth config. You can edit an existing workspace type or create a new one. In this example we will create a new one: + +```yaml +apiVersion: tenancy.kcp.io/v1alpha1 +kind: WorkspaceType +metadata: + name: with-auth +spec: + authenticationConfigurations: + - name: my-auth-config +``` + +Remember that changing a workspace type would affect all existing workspaces, too, not just newly created ones. + +### Step 3: Workspaces + +Now we're already create to create workspaces using the new type. This can be done on the command line: + +```bash +kubectl create workspace my-workspace --type with-auth +``` + +### Step 4: Authorization + +It's now time to configure permissions for your new users. Depending on the configuration and claims in the auth config, a suitable `ClusterRoleBinding` could look like this: + +```yaml +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: make-externals-admins +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: cluster-admin +subjects: + - apiGroup: rbac.authorization.k8s.io + kind: Group + name: oidc:admins +``` + +The CRB above would grant all users in the `admins` group cluster-admin permission inside the workspace. + +Create the ClusterRoleBinding inside the new workspace: + +```bash +kubectl ws :root:my-workspace +kubectl apply --filename clusterrolebinding.yaml +``` + +### Step 5: Testing + +Your setup is now complete. You can take a token produced by your OIDC provider (remember that it needs to include both kcp's global audience and your own audience settings) and authenticate to your workspace. diff --git a/go.mod b/go.mod index 9be4c23d93c..a69af9a6722 100644 --- a/go.mod +++ b/go.mod @@ -16,6 +16,7 @@ require ( github.com/evanphx/json-patch v5.6.0+incompatible github.com/fatih/color v1.18.0 github.com/go-logr/logr v1.4.2 + github.com/golang-jwt/jwt/v5 v5.2.0 github.com/google/go-cmp v0.7.0 github.com/google/uuid v1.6.0 github.com/kcp-dev/apimachinery/v2 v2.0.1-0.20250728122101-adbf20db3e51 @@ -30,6 +31,7 @@ require ( github.com/spf13/cobra v1.9.1 github.com/spf13/pflag v1.0.6 github.com/stretchr/testify v1.10.0 + github.com/xrstf/mockoidc v0.0.0-20250721141841-711cc4e835f6 go.uber.org/goleak v1.3.1-0.20241121203838-4ff5fa6529ee go.uber.org/multierr v1.11.0 golang.org/x/sys v0.33.0 @@ -68,6 +70,7 @@ require ( github.com/felixge/httpsnoop v1.0.4 // indirect github.com/fsnotify/fsnotify v1.7.0 // indirect github.com/fxamacker/cbor/v2 v2.7.0 // indirect + github.com/go-jose/go-jose/v3 v3.0.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/go-logr/zapr v1.3.0 // indirect github.com/go-openapi/jsonpointer v0.21.0 // indirect diff --git a/go.sum b/go.sum index b7544131660..86e2e14f447 100644 --- a/go.sum +++ b/go.sum @@ -68,6 +68,8 @@ github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyT github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E= github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/go-jose/go-jose/v3 v3.0.3 h1:fFKWeig/irsp7XD2zBxvnmA/XaRWp5V3CBsZXJF7G7k= +github.com/go-jose/go-jose/v3 v3.0.3/go.mod h1:5b+7YgP7ZICgJDBdfjZaIt+H/9L9T/YQrVfLAMboGkQ= github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= @@ -91,6 +93,8 @@ github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI= github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= +github.com/golang-jwt/jwt/v5 v5.2.0 h1:d/ix8ftRUorsN+5eMIlF4T6J8CAt9rch3My2winC1Jw= +github.com/golang-jwt/jwt/v5 v5.2.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= @@ -303,8 +307,11 @@ github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= github.com/xiang90/probing v0.0.0-20221125231312-a49e3df8f510 h1:S2dVYn90KE98chqDkyE9Z4N61UnQd+KOfgp5Iu53llk= github.com/xiang90/probing v0.0.0-20221125231312-a49e3df8f510/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= +github.com/xrstf/mockoidc v0.0.0-20250721141841-711cc4e835f6 h1:aXc3L+sbFNXV785UIIekjYyeTx8yiHUZCiomTjTqLhc= +github.com/xrstf/mockoidc v0.0.0-20250721141841-711cc4e835f6/go.mod h1:VqIgTtKVzCNAuPra9ps3kr2REpbKpXlP2BtrvWlfnhs= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= go.etcd.io/bbolt v1.3.11 h1:yGEzV1wPz2yVCLsD8ZAiGHhHVlczyC9d1rP43/VCRJ0= go.etcd.io/bbolt v1.3.11/go.mod h1:dksAq7YMXoljX0xu6VF5DMZGbhYYoLUalEiSySYAS4I= go.etcd.io/etcd/api/v3 v3.5.21 h1:A6O2/JDb3tvHhiIz3xf9nJ7REHvtEFJJ3veW3FbCnS8= @@ -355,6 +362,8 @@ go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8= golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= @@ -365,6 +374,8 @@ golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvx golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU= golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -378,7 +389,11 @@ golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLL golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201202161906-c7110b5ffcbb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20211123203042-d83791d6bcd9/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY= golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= @@ -391,6 +406,8 @@ golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ= golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -405,18 +422,32 @@ golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= +golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= golang.org/x/term v0.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg= golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4= golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA= golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0= @@ -429,6 +460,8 @@ golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBn golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/tools v0.33.0 h1:4qz2S3zmRxbGIhDIAgjxvFutSvH5EfnsYrRBj0UI0bc= golang.org/x/tools v0.33.0/go.mod h1:CIJMaWEY88juyUfo7UbgPqbC8rU2OqfAV1h2Qp0oMYI= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/pkg/authentication/authentication_controller.go b/pkg/authentication/authentication_controller.go new file mode 100644 index 00000000000..daca9d40ebf --- /dev/null +++ b/pkg/authentication/authentication_controller.go @@ -0,0 +1,331 @@ +/* +Copyright 2025 The KCP Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package authentication + +import ( + "context" + "fmt" + "sync" + "time" + + "k8s.io/apimachinery/pkg/api/errors" + utilruntime "k8s.io/apimachinery/pkg/util/runtime" + "k8s.io/apimachinery/pkg/util/wait" + "k8s.io/apiserver/pkg/authentication/authenticator" + "k8s.io/client-go/tools/cache" + "k8s.io/client-go/util/workqueue" + "k8s.io/klog/v2" + + "github.com/kcp-dev/logicalcluster/v3" + + "github.com/kcp-dev/kcp/pkg/logging" + corev1alpha1 "github.com/kcp-dev/kcp/sdk/apis/core/v1alpha1" + tenancyv1alpha1 "github.com/kcp-dev/kcp/sdk/apis/tenancy/v1alpha1" + kcpclientset "github.com/kcp-dev/kcp/sdk/client/clientset/versioned/cluster" + corev1alpha1informers "github.com/kcp-dev/kcp/sdk/client/informers/externalversions/core/v1alpha1" + tenancyv1alpha1informers "github.com/kcp-dev/kcp/sdk/client/informers/externalversions/tenancy/v1alpha1" + corev1alpha1listers "github.com/kcp-dev/kcp/sdk/client/listers/core/v1alpha1" +) + +const ( + controllerName = "kcp-workspace-authentication-index" + + resyncPeriod = 2 * time.Hour +) + +type ClusterClientGetter func(shard *corev1alpha1.Shard) (kcpclientset.ClusterInterface, error) + +// Controller watches Shards on the root shard, and then starts informers +// for every Shard, watching the Workspaces, their types and their authentication configurations on +// them. It then updates the workspace index, which maps logical clusters to their authenticators. +// +// This controller is very much inspired by the workspace index controller, but is its own thing +// because of the additional complexity of recursively resolving workspace types. +type Controller struct { + queue workqueue.TypedRateLimitingInterface[string] + + clientGetter ClusterClientGetter + authIndex state + + shardIndexer cache.Indexer + shardLister corev1alpha1listers.ShardLister + + lock sync.RWMutex + shardWatchers map[string]*shardWatcher +} + +func NewController( + ctx context.Context, + shardInformer corev1alpha1informers.ShardInformer, + clientGetter ClusterClientGetter, + baseAudiences authenticator.Audiences, +) (*Controller, error) { + queue := workqueue.NewTypedRateLimitingQueueWithConfig( + workqueue.DefaultTypedControllerRateLimiter[string](), + workqueue.TypedRateLimitingQueueConfig[string]{ + Name: "controllerName", + }, + ) + + c := &Controller{ + queue: queue, + + clientGetter: clientGetter, + authIndex: *NewIndex(ctx, baseAudiences), + + shardIndexer: shardInformer.Informer().GetIndexer(), + shardLister: shardInformer.Lister(), + + shardWatchers: map[string]*shardWatcher{}, + } + + _, err := shardInformer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{ + AddFunc: func(obj interface{}) { + shard := obj.(*corev1alpha1.Shard) + c.enqueueShard(ctx, shard) + }, + UpdateFunc: func(old, obj interface{}) { + shard := obj.(*corev1alpha1.Shard) + oldShard := old.(*corev1alpha1.Shard) + if oldShard.Spec.BaseURL == shard.Spec.BaseURL { + return + } + c.stopShard(oldShard.Name) + c.enqueueShard(ctx, shard) + }, + DeleteFunc: func(obj interface{}) { + if final, ok := obj.(cache.DeletedFinalStateUnknown); ok { + obj = final.Obj + } + shard := obj.(*corev1alpha1.Shard) + + c.stopShard(shard.Name) + }, + }) + if err != nil { + return nil, fmt.Errorf("failed to start Shard informer: %w", err) + } + + return c, nil +} + +// Start the controller. It does not really do anything, but to keep the shape of a normal +// controller, we keep it. +func (c *Controller) Start(ctx context.Context, numThreads int) { + defer utilruntime.HandleCrash() + defer func() { + c.lock.Lock() + defer c.lock.Unlock() + for _, watcher := range c.shardWatchers { + watcher.Stop() + } + }() + + logger := klog.FromContext(ctx).WithValues("controller", controllerName) + logger.Info("Starting controller") + defer logger.Info("Shutting down controller") + + for range numThreads { + go wait.UntilWithContext(ctx, c.startWorker, time.Second) + } + + <-ctx.Done() +} + +func (c *Controller) enqueueShard(ctx context.Context, obj interface{}) { + key, err := cache.DeletionHandlingMetaNamespaceKeyFunc(obj) + if err != nil { + utilruntime.HandleError(err) + return + } + + logger := klog.FromContext(ctx) + logger.WithValues("key", key).Info("enqueueing Shard") + + c.queue.Add(key) +} + +func (c *Controller) startWorker(ctx context.Context) { + for c.processNextWorkItem(ctx) { + } +} + +func (c *Controller) processNextWorkItem(ctx context.Context) bool { + // Wait until there is a new item in the working queue + k, quit := c.queue.Get() + if quit { + return false + } + key := k + + logger := logging.WithQueueKey(klog.FromContext(ctx), key) + ctx = klog.NewContext(ctx, logger) + logger.V(4).Info("processing key") + + // No matter what, tell the queue we're done with this key, to unblock + // other workers. + defer c.queue.Done(key) + + if err := c.process(ctx, key); err != nil { + utilruntime.HandleError(fmt.Errorf("%q controller failed to sync %q, err: %w", controllerName, key, err)) + c.queue.AddRateLimited(key) + return true + } + c.queue.Forget(key) + return true +} + +func (c *Controller) process(ctx context.Context, key string) error { + logger := klog.FromContext(ctx) + + _, name, err := cache.SplitMetaNamespaceKey(key) + if err != nil { + utilruntime.HandleError(err) + return nil + } + shard, err := c.shardLister.Get(name) + if err != nil { + if errors.IsNotFound(err) { + logger.V(2).Info("Shard not found, stopping informers") + c.stopShard(name) + return nil + } + return err + } + + c.lock.Lock() + defer c.lock.Unlock() + + if _, found := c.shardWatchers[shard.Name]; !found { + logger.V(2).Info("Starting informers for Shard") + + client, err := c.clientGetter(shard) + if err != nil { + return err + } + + watcher, err := NewShardWatcher(ctx, shard.Name, client, &c.authIndex) + if err != nil { + return fmt.Errorf("failed to start shard watcher: %w", err) + } + + c.shardWatchers[shard.Name] = watcher + } + + return nil +} + +func (c *Controller) stopShard(shardName string) { + c.authIndex.DeleteShard(shardName) + + c.lock.Lock() + defer c.lock.Unlock() + + if watcher, ok := c.shardWatchers[shardName]; ok { + watcher.Stop() + delete(c.shardWatchers, shardName) + } +} + +func (c *Controller) Lookup(wsType logicalcluster.Path) (authenticator.Request, bool) { + return c.authIndex.Lookup(wsType) +} + +type shardWatcher struct { + state *state + workspaceTypeInformer cache.SharedIndexInformer + workspaceAuthConfigInformer cache.SharedIndexInformer + cancel context.CancelFunc +} + +func NewShardWatcher( + ctx context.Context, + shardName string, + shardClient kcpclientset.ClusterInterface, + state *state, +) (*shardWatcher, error) { + wacInformer := tenancyv1alpha1informers.NewWorkspaceAuthenticationConfigurationClusterInformer(shardClient, resyncPeriod, nil) + _, err := wacInformer.AddEventHandler(cache.ResourceEventHandlerFuncs{ + AddFunc: func(obj interface{}) { + wac := obj.(*tenancyv1alpha1.WorkspaceAuthenticationConfiguration) + state.UpsertWorkspaceAuthenticationConfiguration(shardName, wac) + }, + UpdateFunc: func(old, obj interface{}) { + wac := obj.(*tenancyv1alpha1.WorkspaceAuthenticationConfiguration) + state.UpsertWorkspaceAuthenticationConfiguration(shardName, wac) + }, + DeleteFunc: func(obj interface{}) { + if final, ok := obj.(cache.DeletedFinalStateUnknown); ok { + obj = final.Obj + } + wac := obj.(*tenancyv1alpha1.WorkspaceAuthenticationConfiguration) + state.DeleteWorkspaceAuthenticationConfiguration(shardName, wac) + }, + }) + if err != nil { + return nil, fmt.Errorf("failed to start WorkspaceAuthenticationConfiguration informer: %w", err) + } + + wtInformer := tenancyv1alpha1informers.NewWorkspaceTypeClusterInformer(shardClient, resyncPeriod, nil) + _, err = wtInformer.AddEventHandler(cache.ResourceEventHandlerFuncs{ + AddFunc: func(obj interface{}) { + wt := obj.(*tenancyv1alpha1.WorkspaceType) + state.UpsertWorkspaceType(shardName, wt) + }, + UpdateFunc: func(old, obj interface{}) { + wt := obj.(*tenancyv1alpha1.WorkspaceType) + state.UpsertWorkspaceType(shardName, wt) + }, + DeleteFunc: func(obj interface{}) { + if final, ok := obj.(cache.DeletedFinalStateUnknown); ok { + obj = final.Obj + } + wt := obj.(*tenancyv1alpha1.WorkspaceType) + state.DeleteWorkspaceType(shardName, wt) + }, + }) + if err != nil { + return nil, fmt.Errorf("failed to start WorkspaceType informer: %w", err) + } + + ctx, cancel := context.WithCancel(ctx) + + watcher := &shardWatcher{ + state: state, + workspaceTypeInformer: wtInformer, + workspaceAuthConfigInformer: wacInformer, + cancel: cancel, + } + + go wacInformer.Run(ctx.Done()) + go wtInformer.Run(ctx.Done()) + + // no need to wait. We only care about events and they arrive when they arrive. + + return watcher, nil +} + +func (w *shardWatcher) Stop() { + if w.cancel != nil { + w.cancel() + w.cancel = nil + } +} + +func (w *shardWatcher) Lookup(wsType logicalcluster.Path) (authenticator.Request, bool) { + return w.state.Lookup(wsType) +} diff --git a/pkg/authentication/authenticators.go b/pkg/authentication/authenticators.go new file mode 100644 index 00000000000..96365c46590 --- /dev/null +++ b/pkg/authentication/authenticators.go @@ -0,0 +1,64 @@ +/* +Copyright 2025 The KCP Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package authentication + +import ( + "fmt" + "net/http" + + "k8s.io/apiserver/pkg/authentication/authenticator" + "k8s.io/apiserver/pkg/authentication/user" + + "github.com/kcp-dev/kcp/pkg/proxy/lookup" +) + +func NewWorkspaceAuthenticator() authenticator.Request { + return authenticator.RequestFunc(func(req *http.Request) (*authenticator.Response, bool, error) { + reqAuthenticator, ok := WorkspaceAuthenticatorFrom(req.Context()) + if !ok { + return nil, false, nil + } + + return reqAuthenticator.AuthenticateRequest(req) + }) +} + +func withClusterScope(delegate authenticator.Request) authenticator.Request { + return authenticator.RequestFunc(func(req *http.Request) (*authenticator.Response, bool, error) { + response, authenticated, ok := delegate.AuthenticateRequest(req) + if authenticated { + cluster := lookup.ClusterNameFrom(req.Context()) + + extra := response.User.GetExtra() + if extra == nil { + extra = map[string][]string{} + } + if true { + extra["authentication.kcp.io/scopes"] = append(extra["authentication.kcp.io/scopes"], fmt.Sprintf("cluster:%s", cluster)) + } + + response.User = &user.DefaultInfo{ + Name: response.User.GetName(), + UID: response.User.GetUID(), + Groups: response.User.GetGroups(), + Extra: extra, + } + } + + return response, authenticated, ok + }) +} diff --git a/pkg/authentication/filters.go b/pkg/authentication/filters.go new file mode 100644 index 00000000000..bacd5be4430 --- /dev/null +++ b/pkg/authentication/filters.go @@ -0,0 +1,70 @@ +/* +Copyright 2025 The KCP Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package authentication + +import ( + "context" + "net/http" + + "k8s.io/apiserver/pkg/authentication/authenticator" + + "github.com/kcp-dev/kcp/pkg/proxy/lookup" +) + +// WithWorkspaceAuthResolver looks up the target cluster in the given auth index +// to populate a possible workspace authenticator in the request's context. This +// is used to let other middlewares know about the existence of additional auth +// options. +func WithWorkspaceAuthResolver(handler http.Handler, authIndex AuthenticatorIndex) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + wsType := lookup.WorkspaceTypeFrom(req.Context()) + if wsType.Empty() { + handler.ServeHTTP(w, req) + return + } + + authn, ok := authIndex.Lookup(wsType) + if !ok { + handler.ServeHTTP(w, req) + return + } + + // make the authenticator always add the target cluster to the user scopes + authn = withClusterScope(authn) + + req = req.WithContext(WithWorkspaceAuthenticator(req.Context(), authn)) + handler.ServeHTTP(w, req) + }) +} + +type contextKey int + +const ( + authenticatorContextKey contextKey = iota +) + +func WithWorkspaceAuthenticator(parent context.Context, authenticator authenticator.Request) context.Context { + return context.WithValue(parent, authenticatorContextKey, authenticator) +} + +func WorkspaceAuthenticatorFrom(ctx context.Context) (authenticator.Request, bool) { + authenticator, ok := ctx.Value(authenticatorContextKey).(authenticator.Request) + if !ok { + return nil, false + } + return authenticator, true +} diff --git a/pkg/proxy/authentication/groups.go b/pkg/authentication/groups.go similarity index 100% rename from pkg/proxy/authentication/groups.go rename to pkg/authentication/groups.go diff --git a/pkg/proxy/authentication/groups_test.go b/pkg/authentication/groups_test.go similarity index 100% rename from pkg/proxy/authentication/groups_test.go rename to pkg/authentication/groups_test.go diff --git a/pkg/authentication/index.go b/pkg/authentication/index.go new file mode 100644 index 00000000000..74e97a633f8 --- /dev/null +++ b/pkg/authentication/index.go @@ -0,0 +1,248 @@ +/* +Copyright 2025 The KCP Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package authentication + +import ( + "context" + "errors" + "fmt" + "sync" + + "k8s.io/apiserver/pkg/authentication/authenticator" + authenticatorunion "k8s.io/apiserver/pkg/authentication/request/union" + "k8s.io/klog/v2" + kubeauthenticator "k8s.io/kubernetes/pkg/kubeapiserver/authenticator" + + "github.com/kcp-dev/logicalcluster/v3" + + "github.com/kcp-dev/kcp/sdk/apis/core" + tenancyv1alpha1 "github.com/kcp-dev/kcp/sdk/apis/tenancy/v1alpha1" +) + +// AuthenticatorIndex implements a mapping from workspace type to authenticator.Request. +type AuthenticatorIndex interface { + Lookup(wsType logicalcluster.Path) (authenticator.Request, bool) +} + +// state keeps track of authenticators for each workspace type. +type state struct { + lock sync.RWMutex + // This component's job is to hand the application's long-lived context to new, + // ad-hoc started goroutines, so it must store the context here. The context + // available for a reconciliation will be cancelled too early and is not suitable + // for authenticators. + //nolint:containedctx + lifecycleCtx context.Context + baseAudiences authenticator.Audiences + workspaceTypeAuthenticators map[string]map[logicalcluster.Path][]authenticatorKey + authConfigAuthenticators map[string]map[authenticatorKey]authenticatorState +} + +type authenticatorKey struct { + cluster logicalcluster.Name + name string +} + +type authenticatorState struct { + cancel context.CancelCauseFunc + authenticator authenticator.Request +} + +func NewIndex(lifecycleCtx context.Context, baseAudiences authenticator.Audiences) *state { + return &state{ + lifecycleCtx: lifecycleCtx, + workspaceTypeAuthenticators: map[string]map[logicalcluster.Path][]authenticatorKey{}, + authConfigAuthenticators: map[string]map[authenticatorKey]authenticatorState{}, + baseAudiences: baseAudiences, + } +} + +func (c *state) UpsertWorkspaceType(shard string, wst *tenancyv1alpha1.WorkspaceType) { + c.lock.Lock() + defer c.lock.Unlock() + + clusterName := logicalcluster.From(wst) + + authenticators := []authenticatorKey{} + for _, authConfig := range wst.Spec.AuthenticationConfigurations { + authenticators = append(authenticators, authenticatorKey{ + cluster: clusterName, + name: authConfig.Name, + }) + } + + wstKey := getWorkspaceTypeKey(wst) + + if c.workspaceTypeAuthenticators[shard] == nil { + c.workspaceTypeAuthenticators[shard] = map[logicalcluster.Path][]authenticatorKey{} + } + c.workspaceTypeAuthenticators[shard][wstKey] = authenticators +} + +func getWorkspaceTypeKey(wst *tenancyv1alpha1.WorkspaceType) logicalcluster.Path { + return logicalcluster.NewPath(wst.Annotations[core.LogicalClusterPathAnnotationKey]).Join(wst.Name) +} + +func (c *state) DeleteWorkspaceType(shard string, wst *tenancyv1alpha1.WorkspaceType) { + wstKey := getWorkspaceTypeKey(wst) + + c.lock.Lock() + defer c.lock.Unlock() + + delete(c.workspaceTypeAuthenticators[shard], wstKey) + if len(c.workspaceTypeAuthenticators[shard]) == 0 { + delete(c.workspaceTypeAuthenticators, shard) + } +} + +var ( + errCauseUpsert = errors.New("authentication configuration has changed") + errCauseDelete = errors.New("authentication configuration has been deleted") + errCauseDeleteShard = errors.New("shard has been deleted") + errCauseEmpty = errors.New("no valid authentication methods configured") +) + +func (c *state) UpsertWorkspaceAuthenticationConfiguration(shard string, authConfig *tenancyv1alpha1.WorkspaceAuthenticationConfiguration) { + mapKey := getAuthConfigKey(authConfig) + + // Stop and delete the old authenticator; do not lock for the entire duration of this function + // because initializing the authenticator later might be comparatively slow. + c.lock.Lock() + c.stopAuthenticator(shard, mapKey, errCauseUpsert) + c.lock.Unlock() + + // build new authenticator + kubeAuthConfig := kubeauthenticator.Config{ + AuthenticationConfig: convertAuthenticationConfiguration(authConfig), + APIAudiences: c.baseAudiences, + } + + ctx, cancel := context.WithCancelCause(c.lifecycleCtx) + + authn, _, _, _, err := kubeAuthConfig.New(ctx) + if err != nil { + logger := klog.FromContext(ctx).WithValues("controller", controllerName) + logger.Error(err, "Failed to start workspace authenticator.") + + cancel(fmt.Errorf("authenticator failed to start: %w", err)) + return + } + + // nil is returned whenever no valid individual auth method is configured. + if authn == nil { + cancel(errCauseEmpty) + return + } + + c.lock.Lock() + defer c.lock.Unlock() + + if c.authConfigAuthenticators[shard] == nil { + c.authConfigAuthenticators[shard] = map[authenticatorKey]authenticatorState{} + } + c.authConfigAuthenticators[shard][mapKey] = authenticatorState{ + cancel: cancel, + authenticator: authn, + } +} + +func getAuthConfigKey(authConfig *tenancyv1alpha1.WorkspaceAuthenticationConfiguration) authenticatorKey { + return authenticatorKey{ + cluster: logicalcluster.From(authConfig), + name: authConfig.Name, + } +} + +func (c *state) stopAuthenticator(shard string, key authenticatorKey, cause error) { + authenticator, ok := c.authConfigAuthenticators[shard][key] + if ok { + authenticator.cancel(cause) + delete(c.authConfigAuthenticators[shard], key) + if len(c.authConfigAuthenticators[shard]) == 0 { + delete(c.authConfigAuthenticators, shard) + } + } +} + +func (c *state) DeleteWorkspaceAuthenticationConfiguration(shard string, authConfig *tenancyv1alpha1.WorkspaceAuthenticationConfiguration) { + mapKey := getAuthConfigKey(authConfig) + + // Stop and delete the old authenticator + c.lock.Lock() + defer c.lock.Unlock() + + c.stopAuthenticator(shard, mapKey, errCauseDelete) +} + +func (c *state) DeleteShard(shardName string) { + c.lock.Lock() + defer c.lock.Unlock() + + // stop all authenticators on this shard + for key := range c.authConfigAuthenticators[shardName] { + c.stopAuthenticator(shardName, key, errCauseDeleteShard) + } + + delete(c.workspaceTypeAuthenticators, shardName) + delete(c.authConfigAuthenticators, shardName) +} + +func (c *state) Lookup(wsType logicalcluster.Path) (authenticator.Request, bool) { + var ( + shard string + authenticatorKeys []authenticatorKey + ) + + c.lock.RLock() + defer c.lock.RUnlock() + + for shardKey, authenticatorsMap := range c.workspaceTypeAuthenticators { + var found bool + authenticatorKeys, found = authenticatorsMap[wsType] + if found { + shard = shardKey + break + } + } + + if len(authenticatorKeys) == 0 { + return nil, false + } + + var authenticators []authenticator.Request + for _, key := range authenticatorKeys { + authenticator, ok := c.authConfigAuthenticators[shard][key] + if ok { + authenticators = append(authenticators, authenticator.authenticator) + } + } + + if len(authenticators) == 0 { + return nil, false + } + + authenticator := authenticatorunion.New(authenticators...) + + // ensure that per-workspace auth cannot be used to become a system: user/group + authenticator = ForbidSystemUsernames(authenticator) + filtered := &GroupFilter{ + Authenticator: authenticator, + DropGroupPrefixes: []string{"system:"}, + } + + return filtered, true +} diff --git a/pkg/authentication/index_test.go b/pkg/authentication/index_test.go new file mode 100644 index 00000000000..026fc712b4d --- /dev/null +++ b/pkg/authentication/index_test.go @@ -0,0 +1,104 @@ +/* +Copyright 2025 The KCP Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package authentication + +import ( + "context" + "errors" + "testing" + + "github.com/stretchr/testify/require" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + "github.com/kcp-dev/logicalcluster/v3" + + "github.com/kcp-dev/kcp/pkg/index" + corev1alpha1 "github.com/kcp-dev/kcp/sdk/apis/core/v1alpha1" + tenancyv1alpha1 "github.com/kcp-dev/kcp/sdk/apis/tenancy/v1alpha1" + conditionsv1alpha1 "github.com/kcp-dev/kcp/sdk/apis/third_party/conditions/apis/conditions/v1alpha1" +) + +func TestCrossShardWorkspaceType(t *testing.T) { + const ( + shardName = "shard-1" + teamCluster = "logicalteamcluster" + ) + + ctx, cancel := context.WithCancelCause(context.Background()) + defer cancel(errors.New("test has ended")) + + clusterIndex := index.New(nil) + authIndex := NewIndex(ctx, nil) + + // setup + clusterIndex.UpsertShard("root", "https://root.io") + clusterIndex.UpsertShard(shardName, "https://buck.io") + + // setup the root logicalcluster (has no workspace) + clusterIndex.UpsertLogicalCluster("root", newLogicalCluster("root", "root:root")) + + // place the custom workspace type + authIndex.UpsertWorkspaceType("root", newWorkspaceType("custom-type", "root")) + + // setup the team workspace (ws is on root shard in root workspace, cluster is on the 2nd shard) + ws := newWorkspace("team1", "root", teamCluster) + ws.Spec.Type = &tenancyv1alpha1.WorkspaceTypeReference{ + Name: "custom-type", + Path: "root", + } + clusterIndex.UpsertWorkspace("root", ws) + clusterIndex.UpsertLogicalCluster(shardName, newLogicalCluster(teamCluster, "root:custom-type")) + + r, found := clusterIndex.Lookup(logicalcluster.NewPath("root:team1")) + require.True(t, found) + require.Equal(t, shardName, r.Shard) + require.Equal(t, teamCluster, r.Cluster.String()) + require.Equal(t, "root:custom-type", r.Type.String()) +} + +func newWorkspaceType(name, cluster string) *tenancyv1alpha1.WorkspaceType { + return &tenancyv1alpha1.WorkspaceType{ + ObjectMeta: metav1.ObjectMeta{Name: name, Annotations: map[string]string{"kcp.io/cluster": cluster}}, + Spec: tenancyv1alpha1.WorkspaceTypeSpec{}, + } +} + +func newWorkspace(name, cluster, scheduledCluster string) *tenancyv1alpha1.Workspace { + return &tenancyv1alpha1.Workspace{ + ObjectMeta: metav1.ObjectMeta{Name: name, Annotations: map[string]string{"kcp.io/cluster": cluster}}, + Spec: tenancyv1alpha1.WorkspaceSpec{Cluster: scheduledCluster}, + Status: tenancyv1alpha1.WorkspaceStatus{Phase: corev1alpha1.LogicalClusterPhaseReady}, + } +} + +func WithCondition(ws *tenancyv1alpha1.Workspace, condition conditionsv1alpha1.Condition) *tenancyv1alpha1.Workspace { + ws.Status.Conditions = []conditionsv1alpha1.Condition{condition} + return ws +} + +func newLogicalCluster(cluster string, fqType string) *corev1alpha1.LogicalCluster { + return &corev1alpha1.LogicalCluster{ + ObjectMeta: metav1.ObjectMeta{ + Name: "cluster", + Annotations: map[string]string{ + "kcp.io/cluster": cluster, + tenancyv1alpha1.LogicalClusterTypeAnnotationKey: fqType, + }, + }, + } +} diff --git a/pkg/authentication/kube_conversion.go b/pkg/authentication/kube_conversion.go new file mode 100644 index 00000000000..3c5bec90b85 --- /dev/null +++ b/pkg/authentication/kube_conversion.go @@ -0,0 +1,120 @@ +/* +Copyright 2025 The KCP Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package authentication + +import ( + "k8s.io/apiserver/pkg/apis/apiserver" + + tenancyv1alpha1 "github.com/kcp-dev/kcp/sdk/apis/tenancy/v1alpha1" +) + +// The functions in this file convert the kcp AuthConfig to Kubernetes AuthConfig, which cannot be +// used directly in our CRDs because they do not have json tags. + +func convertAuthenticationConfiguration(in *tenancyv1alpha1.WorkspaceAuthenticationConfiguration) *apiserver.AuthenticationConfiguration { + out := &apiserver.AuthenticationConfiguration{ + JWT: []apiserver.JWTAuthenticator{}, + } + + for _, authn := range in.Spec.JWT { + out.JWT = append(out.JWT, convertJWTAuthenticator(authn)) + } + + return out +} + +func convertJWTAuthenticator(in tenancyv1alpha1.JWTAuthenticator) apiserver.JWTAuthenticator { + out := apiserver.JWTAuthenticator{ + Issuer: convertIssuer(in.Issuer), + ClaimMappings: convertClaimMappings(in.ClaimMappings), + ClaimValidationRules: []apiserver.ClaimValidationRule{}, + UserValidationRules: []apiserver.UserValidationRule{}, + } + + for _, rule := range in.ClaimValidationRules { + out.ClaimValidationRules = append(out.ClaimValidationRules, convertClaimValidationRule(rule)) + } + + for _, rule := range in.UserValidationRules { + out.UserValidationRules = append(out.UserValidationRules, convertUserValidationRule(rule)) + } + + return out +} + +func convertIssuer(in tenancyv1alpha1.Issuer) apiserver.Issuer { + return apiserver.Issuer{ + URL: in.URL, + DiscoveryURL: in.DiscoveryURL, + CertificateAuthority: in.CertificateAuthority, + Audiences: in.Audiences, + AudienceMatchPolicy: apiserver.AudienceMatchPolicyType(in.AudienceMatchPolicy), + } +} + +func convertClaimMappings(in tenancyv1alpha1.ClaimMappings) apiserver.ClaimMappings { + out := apiserver.ClaimMappings{ + Username: convertPrefixedClaimOrExpression(in.Username), + Groups: convertPrefixedClaimOrExpression(in.Groups), + UID: convertClaimOrExpression(in.UID), + Extra: []apiserver.ExtraMapping{}, + } + + for _, mapping := range in.Extra { + out.Extra = append(out.Extra, convertExtraMapping(mapping)) + } + + return out +} + +func convertPrefixedClaimOrExpression(in tenancyv1alpha1.PrefixedClaimOrExpression) apiserver.PrefixedClaimOrExpression { + return apiserver.PrefixedClaimOrExpression{ + Claim: in.Claim, + Prefix: in.Prefix, + Expression: in.Expression, + } +} + +func convertClaimOrExpression(in tenancyv1alpha1.ClaimOrExpression) apiserver.ClaimOrExpression { + return apiserver.ClaimOrExpression{ + Claim: in.Claim, + Expression: in.Expression, + } +} + +func convertClaimValidationRule(in tenancyv1alpha1.ClaimValidationRule) apiserver.ClaimValidationRule { + return apiserver.ClaimValidationRule{ + Claim: in.Claim, + RequiredValue: in.RequiredValue, + Message: in.Message, + Expression: in.Expression, + } +} + +func convertUserValidationRule(in tenancyv1alpha1.UserValidationRule) apiserver.UserValidationRule { + return apiserver.UserValidationRule{ + Message: in.Message, + Expression: in.Expression, + } +} + +func convertExtraMapping(in tenancyv1alpha1.ExtraMapping) apiserver.ExtraMapping { + return apiserver.ExtraMapping{ + Key: in.Key, + ValueExpression: in.ValueExpression, + } +} diff --git a/pkg/authentication/usernames.go b/pkg/authentication/usernames.go new file mode 100644 index 00000000000..7f8cf8ff59a --- /dev/null +++ b/pkg/authentication/usernames.go @@ -0,0 +1,44 @@ +/* +Copyright 2022 The KCP Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package authentication + +import ( + "errors" + "net/http" + "strings" + + "k8s.io/apiserver/pkg/authentication/authenticator" +) + +// ForbidSystemUsernames wraps an authenticator and prevents it from returning +// an internal system username (anything beginning with "system:"). This is so +// per-workspace authenticators cannot impersonate low-level system accounts or +// serviceaccounts. +// This filter should be used together with the GroupsFilter to also strip +// system groups. +func ForbidSystemUsernames(delegate authenticator.Request) authenticator.Request { + return authenticator.RequestFunc(func(req *http.Request) (*authenticator.Response, bool, error) { + result, authenticated, err := delegate.AuthenticateRequest(req) + if err == nil { + if strings.HasPrefix(result.User.GetName(), "system:") { + return nil, false, errors.New("system usernames are not admitted") + } + } + + return result, authenticated, err + }) +} diff --git a/pkg/features/kcp_features.go b/pkg/features/kcp_features.go index f54bbe38007..701efe99ff4 100644 --- a/pkg/features/kcp_features.go +++ b/pkg/features/kcp_features.go @@ -53,6 +53,13 @@ const ( // Enables VirtualWorkspace urls on APIExport. This enables to use Deprecated APIExport VirtualWorkspace urls. // This is a temporary feature to ease the migration to the new VirtualWorkspace urls. EnableDeprecatedAPIExportVirtualWorkspacesUrls featuregate.Feature = "EnableDeprecatedAPIExportVirtualWorkspacesUrls" + + // owner: @xrstf + // alpha: v0.1 + // Enables per-workspace authentication using WorkspaceAuthenticationConfiguration objects in order to admit + // users into workspaces from foreign OIDC issuers. This feature can be individually enabled on each shard and + // the front-proxy. + WorkspaceAuthentication featuregate.Feature = "WorkspaceAuthentication" ) // DefaultFeatureGate exposes the upstream feature gate, but with our gate setting applied. @@ -129,6 +136,9 @@ var defaultVersionedGenericControlPlaneFeatureGates = map[featuregate.Feature]fe EnableDeprecatedAPIExportVirtualWorkspacesUrls: { {Version: version.MustParse("1.32"), Default: false, PreRelease: featuregate.Alpha}, }, + WorkspaceAuthentication: { + {Version: version.MustParse("1.32"), Default: false, PreRelease: featuregate.Alpha}, + }, // inherited features from generic apiserver, relisted here to get a conflict if it is changed // unintentionally on either side: genericfeatures.APIResponseCompression: { diff --git a/pkg/index/index.go b/pkg/index/index.go index 77d89f0b5f6..9f186004fcd 100644 --- a/pkg/index/index.go +++ b/pkg/index/index.go @@ -17,6 +17,7 @@ limitations under the License. package index import ( + "maps" "net/url" "strings" "sync" @@ -41,6 +42,9 @@ type Result struct { Shard string // Cluster canonical path Cluster logicalcluster.Name + // Type is not set for mounted workspaces. For all others this value is the + // fully-qualified name of the type, e.g. "root:universal". + Type logicalcluster.Path // ErrorCode is the HTTP error code to return for the request. // If this is set, the URL and Shard fields are ignored. @@ -51,6 +55,27 @@ type Result struct { // the index data. type PathRewriter func(segments []string) []string +// State is a runtime graph of shards, workspaces, logicalclusters and mounts. +// It usually gets fed by a controller that watches these resources on each +// shard. The state then organizes the graph and offers a workspace path lookup +// that resolves a workspace path to its physical location on a shard and logical +// cluster. +type State struct { + rewriters []PathRewriter + + lock sync.RWMutex + clusterShards map[logicalcluster.Name]string // logical cluster -> shard name + shardClusterWorkspaceNameCluster map[string]map[logicalcluster.Name]map[string]logicalcluster.Name // (shard name, logical cluster, workspace name) -> logical cluster + shardClusterWorkspaceName map[string]map[logicalcluster.Name]string // (shard name, logical cluster) -> workspace name + shardClusterWorkspaceType map[string]map[logicalcluster.Name]logicalcluster.Path // (shard name, logical cluster) -> workspace type + shardClusterParentCluster map[string]map[logicalcluster.Name]logicalcluster.Name // (shard name, logical cluster) -> parent logical cluster + shardBaseURLs map[string]string // shard name -> base URL + // Experimental feature: allow mounts to be used with Workspaces + shardClusterWorkspaceMount map[string]map[logicalcluster.Name]map[string]tenancyv1alpha1.WorkspaceSpec // (shard name, logical cluster, workspace name) -> WorkspaceSpec + + shardClusterWorkspaceNameErrorCode map[string]map[logicalcluster.Name]map[string]int // (shard name, logical cluster, workspace name) -> error code +} + func New(rewriters []PathRewriter) *State { return &State{ rewriters: rewriters, @@ -58,6 +83,7 @@ func New(rewriters []PathRewriter) *State { clusterShards: map[logicalcluster.Name]string{}, shardClusterWorkspaceNameCluster: map[string]map[logicalcluster.Name]map[string]logicalcluster.Name{}, shardClusterWorkspaceName: map[string]map[logicalcluster.Name]string{}, + shardClusterWorkspaceType: map[string]map[logicalcluster.Name]logicalcluster.Path{}, shardClusterParentCluster: map[string]map[logicalcluster.Name]logicalcluster.Name{}, shardBaseURLs: map[string]string{}, // Experimental feature: allow mounts to be used with Workspaces @@ -71,24 +97,6 @@ func New(rewriters []PathRewriter) *State { } } -// State watches Shards on the root shard, and then starts informers -// for every Shard, watching the Workspaces on them. It then -// updates the workspace index, which maps logical clusters to shard URLs. -type State struct { - rewriters []PathRewriter - - lock sync.RWMutex - clusterShards map[logicalcluster.Name]string // logical cluster -> shard name - shardClusterWorkspaceNameCluster map[string]map[logicalcluster.Name]map[string]logicalcluster.Name // (shard name, logical cluster, workspace name) -> logical cluster - shardClusterWorkspaceName map[string]map[logicalcluster.Name]string // (shard name, logical cluster) -> workspace name - shardClusterParentCluster map[string]map[logicalcluster.Name]logicalcluster.Name // (shard name, logical cluster) -> parent logical cluster - shardBaseURLs map[string]string // shard name -> base URL - // Experimental feature: allow mounts to be used with Workspaces - shardClusterWorkspaceMount map[string]map[logicalcluster.Name]map[string]tenancyv1alpha1.WorkspaceSpec // (shard name, logical cluster, workspace name) -> WorkspaceSpec - - shardClusterWorkspaceNameErrorCode map[string]map[logicalcluster.Name]map[string]int // (shard name, logical cluster, workspace name) -> error code -} - func (c *State) UpsertWorkspace(shard string, ws *tenancyv1alpha1.Workspace) { if ws.Status.Phase == corev1alpha1.LogicalClusterPhaseScheduling { return @@ -173,12 +181,14 @@ func (c *State) DeleteWorkspace(shard string, ws *tenancyv1alpha1.Workspace) { delete(c.shardClusterWorkspaceNameCluster, shard) } - delete(c.shardClusterWorkspaceName[shard], logicalcluster.Name(ws.Spec.Cluster)) + cluster := logicalcluster.Name(ws.Spec.Cluster) + + delete(c.shardClusterWorkspaceName[shard], cluster) if len(c.shardClusterWorkspaceName[shard]) == 0 { delete(c.shardClusterWorkspaceName, shard) } - delete(c.shardClusterParentCluster[shard], logicalcluster.Name(ws.Spec.Cluster)) + delete(c.shardClusterParentCluster[shard], cluster) if len(c.shardClusterParentCluster[shard]) == 0 { delete(c.shardClusterParentCluster, shard) } @@ -193,6 +203,7 @@ func (c *State) DeleteWorkspace(shard string, ws *tenancyv1alpha1.Workspace) { } } } + delete(c.shardClusterWorkspaceNameErrorCode[shard][clusterName], ws.Name) if len(c.shardClusterWorkspaceNameErrorCode[shard][clusterName]) == 0 { delete(c.shardClusterWorkspaceNameErrorCode[shard], clusterName) @@ -212,7 +223,16 @@ func (c *State) UpsertLogicalCluster(shard string, logicalCluster *corev1alpha1. if got != shard { c.lock.Lock() defer c.lock.Unlock() + c.clusterShards[clusterName] = shard + + // LogicalClusters are annotated with "path:name" of their workspace's type. + typeIdent := logicalcluster.NewPath(logicalCluster.Annotations[tenancyv1alpha1.LogicalClusterTypeAnnotationKey]) + + if c.shardClusterWorkspaceType[shard] == nil { + c.shardClusterWorkspaceType[shard] = map[logicalcluster.Name]logicalcluster.Path{} + } + c.shardClusterWorkspaceType[shard][clusterName] = typeIdent } } @@ -229,6 +249,11 @@ func (c *State) DeleteLogicalCluster(shard string, logicalCluster *corev1alpha1. if got := c.clusterShards[clusterName]; got == shard { delete(c.clusterShards, clusterName) } + + delete(c.shardClusterWorkspaceType[shard], clusterName) + if len(c.shardClusterWorkspaceType[shard]) == 0 { + delete(c.shardClusterWorkspaceType, shard) + } } } @@ -248,14 +273,14 @@ func (c *State) DeleteShard(shardName string) { c.lock.Lock() defer c.lock.Unlock() - for lc, gotShardName := range c.clusterShards { - if shardName == gotShardName { - delete(c.clusterShards, lc) - } - } + maps.DeleteFunc(c.clusterShards, func(_ logicalcluster.Name, shard string) bool { + return shardName == shard + }) + delete(c.shardClusterWorkspaceNameCluster, shardName) delete(c.shardBaseURLs, shardName) delete(c.shardClusterWorkspaceName, shardName) + delete(c.shardClusterWorkspaceType, shardName) delete(c.shardClusterParentCluster, shardName) delete(c.shardClusterWorkspaceNameErrorCode, shardName) } @@ -270,9 +295,12 @@ func (c *State) Lookup(path logicalcluster.Path) (Result, bool) { c.lock.RLock() defer c.lock.RUnlock() - var shard string - var cluster logicalcluster.Name - var errorCode int + var ( + shard string + cluster logicalcluster.Name + wsType logicalcluster.Path + errorCode int + ) // walk through index graph to find the final logical cluster and shard for i, s := range segments { @@ -313,7 +341,15 @@ func (c *State) Lookup(path logicalcluster.Path) (Result, bool) { return Result{}, false } } - return Result{Shard: shard, Cluster: cluster, ErrorCode: errorCode}, true + + wsType = c.shardClusterWorkspaceType[shard][cluster] + + return Result{ + Shard: shard, + Cluster: cluster, + Type: wsType, + ErrorCode: errorCode, + }, true } func (c *State) LookupURL(path logicalcluster.Path) (Result, bool) { @@ -335,6 +371,9 @@ func (c *State) LookupURL(path logicalcluster.Path) (Result, bool) { } return Result{ - URL: strings.TrimSuffix(baseURL, "/") + result.Cluster.Path().RequestPath(), + Shard: result.Shard, + Cluster: result.Cluster, + Type: result.Type, + URL: strings.TrimSuffix(baseURL, "/") + result.Cluster.Path().RequestPath(), }, true } diff --git a/pkg/openapi/zz_generated.openapi.go b/pkg/openapi/zz_generated.openapi.go index 58597f3180a..374d6d33d3c 100644 --- a/pkg/openapi/zz_generated.openapi.go +++ b/pkg/openapi/zz_generated.openapi.go @@ -118,10 +118,22 @@ func GetOpenAPIDefinitions(ref common.ReferenceCallback) map[string]common.OpenA "github.com/kcp-dev/kcp/sdk/apis/core/v1alpha1.ShardSpec": schema_sdk_apis_core_v1alpha1_ShardSpec(ref), "github.com/kcp-dev/kcp/sdk/apis/core/v1alpha1.ShardStatus": schema_sdk_apis_core_v1alpha1_ShardStatus(ref), "github.com/kcp-dev/kcp/sdk/apis/tenancy/v1alpha1.APIExportReference": schema_sdk_apis_tenancy_v1alpha1_APIExportReference(ref), + "github.com/kcp-dev/kcp/sdk/apis/tenancy/v1alpha1.AuthenticationConfigurationReference": schema_sdk_apis_tenancy_v1alpha1_AuthenticationConfigurationReference(ref), + "github.com/kcp-dev/kcp/sdk/apis/tenancy/v1alpha1.ClaimMappings": schema_sdk_apis_tenancy_v1alpha1_ClaimMappings(ref), + "github.com/kcp-dev/kcp/sdk/apis/tenancy/v1alpha1.ClaimOrExpression": schema_sdk_apis_tenancy_v1alpha1_ClaimOrExpression(ref), + "github.com/kcp-dev/kcp/sdk/apis/tenancy/v1alpha1.ClaimValidationRule": schema_sdk_apis_tenancy_v1alpha1_ClaimValidationRule(ref), + "github.com/kcp-dev/kcp/sdk/apis/tenancy/v1alpha1.ExtraMapping": schema_sdk_apis_tenancy_v1alpha1_ExtraMapping(ref), + "github.com/kcp-dev/kcp/sdk/apis/tenancy/v1alpha1.Issuer": schema_sdk_apis_tenancy_v1alpha1_Issuer(ref), + "github.com/kcp-dev/kcp/sdk/apis/tenancy/v1alpha1.JWTAuthenticator": schema_sdk_apis_tenancy_v1alpha1_JWTAuthenticator(ref), "github.com/kcp-dev/kcp/sdk/apis/tenancy/v1alpha1.Mount": schema_sdk_apis_tenancy_v1alpha1_Mount(ref), "github.com/kcp-dev/kcp/sdk/apis/tenancy/v1alpha1.ObjectReference": schema_sdk_apis_tenancy_v1alpha1_ObjectReference(ref), + "github.com/kcp-dev/kcp/sdk/apis/tenancy/v1alpha1.PrefixedClaimOrExpression": schema_sdk_apis_tenancy_v1alpha1_PrefixedClaimOrExpression(ref), + "github.com/kcp-dev/kcp/sdk/apis/tenancy/v1alpha1.UserValidationRule": schema_sdk_apis_tenancy_v1alpha1_UserValidationRule(ref), "github.com/kcp-dev/kcp/sdk/apis/tenancy/v1alpha1.VirtualWorkspace": schema_sdk_apis_tenancy_v1alpha1_VirtualWorkspace(ref), "github.com/kcp-dev/kcp/sdk/apis/tenancy/v1alpha1.Workspace": schema_sdk_apis_tenancy_v1alpha1_Workspace(ref), + "github.com/kcp-dev/kcp/sdk/apis/tenancy/v1alpha1.WorkspaceAuthenticationConfiguration": schema_sdk_apis_tenancy_v1alpha1_WorkspaceAuthenticationConfiguration(ref), + "github.com/kcp-dev/kcp/sdk/apis/tenancy/v1alpha1.WorkspaceAuthenticationConfigurationList": schema_sdk_apis_tenancy_v1alpha1_WorkspaceAuthenticationConfigurationList(ref), + "github.com/kcp-dev/kcp/sdk/apis/tenancy/v1alpha1.WorkspaceAuthenticationConfigurationSpec": schema_sdk_apis_tenancy_v1alpha1_WorkspaceAuthenticationConfigurationSpec(ref), "github.com/kcp-dev/kcp/sdk/apis/tenancy/v1alpha1.WorkspaceList": schema_sdk_apis_tenancy_v1alpha1_WorkspaceList(ref), "github.com/kcp-dev/kcp/sdk/apis/tenancy/v1alpha1.WorkspaceLocation": schema_sdk_apis_tenancy_v1alpha1_WorkspaceLocation(ref), "github.com/kcp-dev/kcp/sdk/apis/tenancy/v1alpha1.WorkspaceSpec": schema_sdk_apis_tenancy_v1alpha1_WorkspaceSpec(ref), @@ -4001,6 +4013,280 @@ func schema_sdk_apis_tenancy_v1alpha1_APIExportReference(ref common.ReferenceCal } } +func schema_sdk_apis_tenancy_v1alpha1_AuthenticationConfigurationReference(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "AuthenticationConfigurationReference provides the fields necessary to resolve a WorkspaceAuthenticationConfiguration.", + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "name": { + SchemaProps: spec.SchemaProps{ + Description: "name is the name of the WorkspaceAuthenticationConfiguration.", + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + }, + Required: []string{"name"}, + }, + }, + } +} + +func schema_sdk_apis_tenancy_v1alpha1_ClaimMappings(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "ClaimMappings provides the configuration for claim mapping.", + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "username": { + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("github.com/kcp-dev/kcp/sdk/apis/tenancy/v1alpha1.PrefixedClaimOrExpression"), + }, + }, + "groups": { + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("github.com/kcp-dev/kcp/sdk/apis/tenancy/v1alpha1.PrefixedClaimOrExpression"), + }, + }, + "uid": { + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("github.com/kcp-dev/kcp/sdk/apis/tenancy/v1alpha1.ClaimOrExpression"), + }, + }, + "extra": { + SchemaProps: spec.SchemaProps{ + Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("github.com/kcp-dev/kcp/sdk/apis/tenancy/v1alpha1.ExtraMapping"), + }, + }, + }, + }, + }, + }, + Required: []string{"username", "groups"}, + }, + }, + Dependencies: []string{ + "github.com/kcp-dev/kcp/sdk/apis/tenancy/v1alpha1.ClaimOrExpression", "github.com/kcp-dev/kcp/sdk/apis/tenancy/v1alpha1.ExtraMapping", "github.com/kcp-dev/kcp/sdk/apis/tenancy/v1alpha1.PrefixedClaimOrExpression"}, + } +} + +func schema_sdk_apis_tenancy_v1alpha1_ClaimOrExpression(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "ClaimOrExpression provides the configuration for a single claim or expression.", + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "claim": { + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "expression": { + SchemaProps: spec.SchemaProps{ + Type: []string{"string"}, + Format: "", + }, + }, + }, + Required: []string{"claim"}, + }, + }, + } +} + +func schema_sdk_apis_tenancy_v1alpha1_ClaimValidationRule(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "ClaimValidationRule provides the configuration for a single claim validation rule.", + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "claim": { + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "requiredValue": { + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "expression": { + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "message": { + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + }, + Required: []string{"claim", "requiredValue", "expression", "message"}, + }, + }, + } +} + +func schema_sdk_apis_tenancy_v1alpha1_ExtraMapping(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "ExtraMapping provides the configuration for a single extra mapping.", + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "key": { + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "valueExpression": { + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + }, + Required: []string{"key", "valueExpression"}, + }, + }, + } +} + +func schema_sdk_apis_tenancy_v1alpha1_Issuer(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "Issuer provides the configuration for an external provider's specific settings.", + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "url": { + SchemaProps: spec.SchemaProps{ + Description: "url points to the issuer URL in a format https://url or https://url/path. This must match the \"iss\" claim in the presented JWT, and the issuer returned from discovery. Same value as the --oidc-issuer-url flag. Discovery information is fetched from \"{url}/.well-known/openid-configuration\" unless overridden by discoveryURL. Required to be unique across all JWT authenticators. Note that egress selection configuration is not used for this network connection.", + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "discoveryURL": { + SchemaProps: spec.SchemaProps{ + Description: "discoveryURL, if specified, overrides the URL used to fetch discovery information instead of using \"{url}/.well-known/openid-configuration\". The exact value specified is used, so \"/.well-known/openid-configuration\" must be included in discoveryURL if needed.\n\nThe \"issuer\" field in the fetched discovery information must match the \"issuer.url\" field in the AuthenticationConfiguration and will be used to validate the \"iss\" claim in the presented JWT. This is for scenarios where the well-known and jwks endpoints are hosted at a different location than the issuer (such as locally in the cluster).\n\nExample: A discovery url that is exposed using kubernetes service 'oidc' in namespace 'oidc-namespace' and discovery information is available at '/.well-known/openid-configuration'. discoveryURL: \"https://oidc.oidc-namespace/.well-known/openid-configuration\" certificateAuthority is used to verify the TLS connection and the hostname on the leaf certificate must be set to 'oidc.oidc-namespace'.\n\ncurl https://oidc.oidc-namespace/.well-known/openid-configuration (.discoveryURL field) {\n issuer: \"https://oidc.example.com\" (.url field)\n}\n\ndiscoveryURL must be different from url. Required to be unique across all JWT authenticators. Note that egress selection configuration is not used for this network connection.", + Type: []string{"string"}, + Format: "", + }, + }, + "certificateAuthority": { + SchemaProps: spec.SchemaProps{ + Type: []string{"string"}, + Format: "", + }, + }, + "audiences": { + SchemaProps: spec.SchemaProps{ + Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + }, + }, + }, + "audienceMatchPolicy": { + SchemaProps: spec.SchemaProps{ + Type: []string{"string"}, + Format: "", + }, + }, + }, + Required: []string{"url"}, + }, + }, + } +} + +func schema_sdk_apis_tenancy_v1alpha1_JWTAuthenticator(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "issuer": { + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("github.com/kcp-dev/kcp/sdk/apis/tenancy/v1alpha1.Issuer"), + }, + }, + "claimValidationRules": { + SchemaProps: spec.SchemaProps{ + Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("github.com/kcp-dev/kcp/sdk/apis/tenancy/v1alpha1.ClaimValidationRule"), + }, + }, + }, + }, + }, + "claimMappings": { + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("github.com/kcp-dev/kcp/sdk/apis/tenancy/v1alpha1.ClaimMappings"), + }, + }, + "userValidationRules": { + SchemaProps: spec.SchemaProps{ + Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("github.com/kcp-dev/kcp/sdk/apis/tenancy/v1alpha1.UserValidationRule"), + }, + }, + }, + }, + }, + }, + Required: []string{"issuer", "claimMappings"}, + }, + }, + Dependencies: []string{ + "github.com/kcp-dev/kcp/sdk/apis/tenancy/v1alpha1.ClaimMappings", "github.com/kcp-dev/kcp/sdk/apis/tenancy/v1alpha1.ClaimValidationRule", "github.com/kcp-dev/kcp/sdk/apis/tenancy/v1alpha1.Issuer", "github.com/kcp-dev/kcp/sdk/apis/tenancy/v1alpha1.UserValidationRule"}, + } +} + func schema_sdk_apis_tenancy_v1alpha1_Mount(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ @@ -4068,6 +4354,67 @@ func schema_sdk_apis_tenancy_v1alpha1_ObjectReference(ref common.ReferenceCallba } } +func schema_sdk_apis_tenancy_v1alpha1_PrefixedClaimOrExpression(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "PrefixedClaimOrExpression provides the configuration for a single prefixed claim or expression.", + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "claim": { + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "prefix": { + SchemaProps: spec.SchemaProps{ + Type: []string{"string"}, + Format: "", + }, + }, + "expression": { + SchemaProps: spec.SchemaProps{ + Type: []string{"string"}, + Format: "", + }, + }, + }, + Required: []string{"claim"}, + }, + }, + } +} + +func schema_sdk_apis_tenancy_v1alpha1_UserValidationRule(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "UserValidationRule provides the configuration for a single user validation rule.", + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "expression": { + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "message": { + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + }, + Required: []string{"expression", "message"}, + }, + }, + } +} + func schema_sdk_apis_tenancy_v1alpha1_VirtualWorkspace(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ @@ -4137,6 +4484,125 @@ func schema_sdk_apis_tenancy_v1alpha1_Workspace(ref common.ReferenceCallback) co } } +func schema_sdk_apis_tenancy_v1alpha1_WorkspaceAuthenticationConfiguration(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "WorkspaceAuthenticationConfiguration specifies additional authentication options for workspaces.", + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "kind": { + SchemaProps: spec.SchemaProps{ + Description: "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", + Type: []string{"string"}, + Format: "", + }, + }, + "apiVersion": { + SchemaProps: spec.SchemaProps{ + Description: "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", + Type: []string{"string"}, + Format: "", + }, + }, + "metadata": { + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"), + }, + }, + "spec": { + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("github.com/kcp-dev/kcp/sdk/apis/tenancy/v1alpha1.WorkspaceAuthenticationConfigurationSpec"), + }, + }, + }, + Required: []string{"metadata", "spec"}, + }, + }, + Dependencies: []string{ + "github.com/kcp-dev/kcp/sdk/apis/tenancy/v1alpha1.WorkspaceAuthenticationConfigurationSpec", "k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"}, + } +} + +func schema_sdk_apis_tenancy_v1alpha1_WorkspaceAuthenticationConfigurationList(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "WorkspaceAuthenticationConfigurationList is a list of WorkspaceAuthenticationConfigurations.", + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "kind": { + SchemaProps: spec.SchemaProps{ + Description: "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", + Type: []string{"string"}, + Format: "", + }, + }, + "apiVersion": { + SchemaProps: spec.SchemaProps{ + Description: "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", + Type: []string{"string"}, + Format: "", + }, + }, + "metadata": { + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.ListMeta"), + }, + }, + "items": { + SchemaProps: spec.SchemaProps{ + Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("github.com/kcp-dev/kcp/sdk/apis/tenancy/v1alpha1.WorkspaceAuthenticationConfiguration"), + }, + }, + }, + }, + }, + }, + Required: []string{"metadata", "items"}, + }, + }, + Dependencies: []string{ + "github.com/kcp-dev/kcp/sdk/apis/tenancy/v1alpha1.WorkspaceAuthenticationConfiguration", "k8s.io/apimachinery/pkg/apis/meta/v1.ListMeta"}, + } +} + +func schema_sdk_apis_tenancy_v1alpha1_WorkspaceAuthenticationConfigurationSpec(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "jwt": { + SchemaProps: spec.SchemaProps{ + Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("github.com/kcp-dev/kcp/sdk/apis/tenancy/v1alpha1.JWTAuthenticator"), + }, + }, + }, + }, + }, + }, + Required: []string{"jwt"}, + }, + }, + Dependencies: []string{ + "github.com/kcp-dev/kcp/sdk/apis/tenancy/v1alpha1.JWTAuthenticator"}, + } +} + func schema_sdk_apis_tenancy_v1alpha1_WorkspaceList(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ @@ -4569,11 +5035,25 @@ func schema_sdk_apis_tenancy_v1alpha1_WorkspaceTypeSpec(ref common.ReferenceCall Format: "", }, }, + "authenticationConfigurations": { + SchemaProps: spec.SchemaProps{ + Description: "authenticationConfigurations are additional authentication options that should apply to any workspace using this workspace type.", + Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("github.com/kcp-dev/kcp/sdk/apis/tenancy/v1alpha1.AuthenticationConfigurationReference"), + }, + }, + }, + }, + }, }, }, }, Dependencies: []string{ - "github.com/kcp-dev/kcp/sdk/apis/tenancy/v1alpha1.APIExportReference", "github.com/kcp-dev/kcp/sdk/apis/tenancy/v1alpha1.WorkspaceTypeExtension", "github.com/kcp-dev/kcp/sdk/apis/tenancy/v1alpha1.WorkspaceTypeReference", "github.com/kcp-dev/kcp/sdk/apis/tenancy/v1alpha1.WorkspaceTypeSelector"}, + "github.com/kcp-dev/kcp/sdk/apis/tenancy/v1alpha1.APIExportReference", "github.com/kcp-dev/kcp/sdk/apis/tenancy/v1alpha1.AuthenticationConfigurationReference", "github.com/kcp-dev/kcp/sdk/apis/tenancy/v1alpha1.WorkspaceTypeExtension", "github.com/kcp-dev/kcp/sdk/apis/tenancy/v1alpha1.WorkspaceTypeReference", "github.com/kcp-dev/kcp/sdk/apis/tenancy/v1alpha1.WorkspaceTypeSelector"}, } } diff --git a/pkg/proxy/filters/filters.go b/pkg/proxy/filters/filters.go index 1e46c8b4031..de3b7464e69 100644 --- a/pkg/proxy/filters/filters.go +++ b/pkg/proxy/filters/filters.go @@ -24,26 +24,51 @@ import ( "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/runtime/serializer" "k8s.io/apiserver/pkg/authentication/authenticator" + authenticatorunion "k8s.io/apiserver/pkg/authentication/request/union" genericapifilters "k8s.io/apiserver/pkg/endpoints/filters" "k8s.io/apiserver/pkg/endpoints/request" "k8s.io/klog/v2" + + "github.com/kcp-dev/kcp/pkg/authentication" ) // WithOptionalAuthentication creates a handler that authenticates a request // if a ClientCert is presented but passes through to the next handler if one is -// not. +// not. It will also call the authenticator present in the request context, if +// any, to support per-workspace authentication. // When additionalAuthMethods is true we also attempt to authenticate even when // no client cert is detected in the request. func WithOptionalAuthentication(handler, failed http.Handler, auth authenticator.Request, additionalAuthMethods bool) http.Handler { - if auth == nil { - return handler - } return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { - if (req.TLS == nil || len(req.TLS.PeerCertificates) == 0) && !additionalAuthMethods { + // Do not accidentally modify the variables in the scope of WithOptionalAuthentication(). + var ( + authenticator = auth + additionalAuth = additionalAuthMethods + ) + + // If an authenticator for the workspace exists, + // it offers an alternative way to authenticate. + reqAuthenticator, ok := authentication.WorkspaceAuthenticatorFrom(req.Context()) + if ok { + if auth != nil { + authenticator = authenticatorunion.New(auth, reqAuthenticator) + } else { + authenticator = reqAuthenticator + } + additionalAuth = true + } + + if authenticator == nil { handler.ServeHTTP(w, req) return } - resp, ok, err := auth.AuthenticateRequest(req) + + if (req.TLS == nil || len(req.TLS.PeerCertificates) == 0) && !additionalAuth { + handler.ServeHTTP(w, req) + return + } + + resp, ok, err := authenticator.AuthenticateRequest(req) if err != nil || !ok { if err != nil { logger := klog.FromContext(req.Context()) diff --git a/pkg/proxy/index/index_controller.go b/pkg/proxy/index/index_controller.go index 9e61442191e..ef0b56ef50d 100644 --- a/pkg/proxy/index/index_controller.go +++ b/pkg/proxy/index/index_controller.go @@ -292,15 +292,5 @@ func (c *Controller) stopShard(shardName string) { } func (c *Controller) LookupURL(path logicalcluster.Path) (index.Result, bool) { - r, found := c.state.LookupURL(path) - if found && r.ErrorCode != 0 { - return index.Result{ - URL: r.URL, - ErrorCode: r.ErrorCode, - }, found - } - return index.Result{ - URL: r.URL, - ErrorCode: 0, - }, found + return c.state.LookupURL(path) } diff --git a/pkg/proxy/handler.go b/pkg/proxy/lookup/lookup.go similarity index 60% rename from pkg/proxy/handler.go rename to pkg/proxy/lookup/lookup.go index ac6f7768b26..d26e29433d1 100644 --- a/pkg/proxy/handler.go +++ b/pkg/proxy/lookup/lookup.go @@ -1,5 +1,5 @@ /* -Copyright 2022 The KCP Authors. +Copyright 2025 The KCP Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -14,9 +14,10 @@ See the License for the specific language governing permissions and limitations under the License. */ -package proxy +package lookup import ( + "context" "net/http" "net/url" "strings" @@ -29,14 +30,14 @@ import ( "github.com/kcp-dev/logicalcluster/v3" kcpauthorization "github.com/kcp-dev/kcp/pkg/authorization" - "github.com/kcp-dev/kcp/pkg/proxy/index" + proxyindex "github.com/kcp-dev/kcp/pkg/proxy/index" ) -func shardHandler(index index.Index, proxy http.Handler) http.HandlerFunc { - return func(w http.ResponseWriter, req *http.Request) { +func WithClusterResolver(delegate http.Handler, index proxyindex.Index) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { var cs = strings.SplitN(strings.TrimLeft(req.URL.Path, "/"), "/", 3) if len(cs) < 2 || cs[0] != "clusters" { - http.NotFound(w, req) + delegate.ServeHTTP(w, req) return } @@ -80,7 +81,54 @@ func shardHandler(index index.Index, proxy http.Handler) http.HandlerFunc { } ctx = WithShardURL(ctx, shardURL) + ctx = WithClusterName(ctx, result.Cluster) + ctx = WithWorkspaceType(ctx, result.Type) req = req.WithContext(ctx) - proxy.ServeHTTP(w, req) + + delegate.ServeHTTP(w, req) + }) +} + +type lookupKey int + +const ( + shardContextKey lookupKey = iota + clusterContextKey + workspaceTypeContextKey +) + +func WithShardURL(parent context.Context, shardURL *url.URL) context.Context { + return context.WithValue(parent, shardContextKey, shardURL) +} + +func ShardURLFrom(ctx context.Context) *url.URL { + shardURL, ok := ctx.Value(shardContextKey).(*url.URL) + if !ok { + return nil + } + return shardURL +} + +func WithClusterName(parent context.Context, cluster logicalcluster.Name) context.Context { + return context.WithValue(parent, clusterContextKey, cluster) +} + +func ClusterNameFrom(ctx context.Context) logicalcluster.Name { + cluster, ok := ctx.Value(clusterContextKey).(logicalcluster.Name) + if !ok { + return "" + } + return cluster +} + +func WithWorkspaceType(parent context.Context, wsType logicalcluster.Path) context.Context { + return context.WithValue(parent, workspaceTypeContextKey, wsType) +} + +func WorkspaceTypeFrom(ctx context.Context) logicalcluster.Path { + cluster, ok := ctx.Value(workspaceTypeContextKey).(logicalcluster.Path) + if !ok { + return logicalcluster.None } + return cluster } diff --git a/pkg/proxy/mapping.go b/pkg/proxy/mapping.go index 03516a57775..af95f5566ca 100644 --- a/pkg/proxy/mapping.go +++ b/pkg/proxy/mapping.go @@ -30,21 +30,28 @@ import ( "k8s.io/klog/v2" "github.com/kcp-dev/kcp/pkg/proxy/index" - proxyoptions "github.com/kcp-dev/kcp/pkg/proxy/options" "github.com/kcp-dev/kcp/pkg/server/proxy" ) -func NewHandler(ctx context.Context, o *proxyoptions.Options, index index.Index) (http.Handler, error) { - mappingData, err := os.ReadFile(o.MappingFile) +func loadMappings(filename string) ([]proxy.PathMapping, error) { + mappingData, err := os.ReadFile(filename) if err != nil { - return nil, fmt.Errorf("failed to read mapping file %q: %w", o.MappingFile, err) + return nil, err } var mapping []proxy.PathMapping - if err = yaml.Unmarshal(mappingData, &mapping); err != nil { - return nil, fmt.Errorf("failed to unmarshal mapping file %q: %w", o.MappingFile, err) + if err := yaml.Unmarshal(mappingData, &mapping); err != nil { + return nil, err } + return mapping, nil +} + +func isShardMapping(m proxy.PathMapping) bool { + return m.Path == "/clusters/" +} + +func NewHandler(ctx context.Context, mappings []proxy.PathMapping, index index.Index) (http.Handler, error) { handlers := proxy.HttpHandler{ Index: index, Mappings: proxy.HttpHandlerMappings{ @@ -57,7 +64,7 @@ func NewHandler(ctx context.Context, o *proxyoptions.Options, index index.Index) } logger := klog.FromContext(ctx) - for _, m := range mapping { + for _, m := range mappings { logger.WithValues("mapping", m).V(2).Info("adding mapping") u, err := url.Parse(m.Backend) @@ -71,10 +78,10 @@ func NewHandler(ctx context.Context, o *proxyoptions.Options, index index.Index) } var handler http.Handler - if m.Path == "/clusters/" { + if isShardMapping(m) { clusterProxy := newShardReverseProxy() clusterProxy.Transport = transport - handler = shardHandler(index, clusterProxy) + handler = clusterProxy } else { // TODO: handle virtual workspace apiservers per shard proxy := httputil.NewSingleHostReverseProxy(u) diff --git a/pkg/proxy/options/authentication.go b/pkg/proxy/options/authentication.go index f778d7f1747..018ca0d6bfa 100644 --- a/pkg/proxy/options/authentication.go +++ b/pkg/proxy/options/authentication.go @@ -36,8 +36,8 @@ import ( kcpkubernetesinformers "github.com/kcp-dev/client-go/informers" kcpkubernetesclientset "github.com/kcp-dev/client-go/kubernetes" + kcpauthentication "github.com/kcp-dev/kcp/pkg/authentication" "github.com/kcp-dev/kcp/pkg/authorization/bootstrap" - kcpauthentication "github.com/kcp-dev/kcp/pkg/proxy/authentication" ) const resyncPeriod = 10 * time.Hour diff --git a/pkg/proxy/proxy.go b/pkg/proxy/proxy.go index b8ddf39b7aa..fc4966adc97 100644 --- a/pkg/proxy/proxy.go +++ b/pkg/proxy/proxy.go @@ -17,7 +17,6 @@ limitations under the License. package proxy import ( - "context" "crypto/tls" "crypto/x509" "fmt" @@ -29,6 +28,8 @@ import ( utilruntime "k8s.io/apimachinery/pkg/util/runtime" userinfo "k8s.io/apiserver/pkg/authentication/user" "k8s.io/apiserver/pkg/endpoints/request" + + "github.com/kcp-dev/kcp/pkg/proxy/lookup" ) func newTransport(clientCert, clientKeyFile, caFile string) (*http.Transport, error) { @@ -85,7 +86,7 @@ func appendClientCertAuthHeaders(header http.Header, user userinfo.Info, userHea func newShardReverseProxy() *httputil.ReverseProxy { director := func(req *http.Request) { - shardURL := ShardURLFrom(req.Context()) + shardURL := lookup.ShardURLFrom(req.Context()) if shardURL == nil { // should not happen if wiring is correct utilruntime.HandleError(fmt.Errorf("no shard URL found in request context")) @@ -100,19 +101,3 @@ func newShardReverseProxy() *httputil.ReverseProxy { } return &httputil.ReverseProxy{Director: director} } - -type shardKey int - -const shardContextKey shardKey = iota - -func WithShardURL(parent context.Context, shardURL *url.URL) context.Context { - return context.WithValue(parent, shardContextKey, shardURL) -} - -func ShardURLFrom(ctx context.Context) *url.URL { - shardURL, ok := ctx.Value(shardContextKey).(*url.URL) - if !ok { - return nil - } - return shardURL -} diff --git a/pkg/proxy/server.go b/pkg/proxy/server.go index a3e248b0d58..8d960155847 100644 --- a/pkg/proxy/server.go +++ b/pkg/proxy/server.go @@ -20,6 +20,7 @@ import ( "context" "fmt" "net/http" + "slices" "time" "k8s.io/apimachinery/pkg/util/wait" @@ -28,8 +29,11 @@ import ( restclient "k8s.io/client-go/rest" "k8s.io/klog/v2" + "github.com/kcp-dev/kcp/pkg/authentication" + kcpfeatures "github.com/kcp-dev/kcp/pkg/features" frontproxyfilters "github.com/kcp-dev/kcp/pkg/proxy/filters" "github.com/kcp-dev/kcp/pkg/proxy/index" + "github.com/kcp-dev/kcp/pkg/proxy/lookup" "github.com/kcp-dev/kcp/pkg/proxy/metrics" kcpfilters "github.com/kcp-dev/kcp/pkg/server/filters" "github.com/kcp-dev/kcp/pkg/server/requestinfo" @@ -45,6 +49,7 @@ type Server struct { CompletedConfig Handler http.Handler IndexController *index.Controller + AuthController *authentication.Controller KcpSharedInformerFactory kcpinformers.SharedScopedInformerFactory } @@ -52,37 +57,83 @@ func NewServer(ctx context.Context, c CompletedConfig) (*Server, error) { s := &Server{ CompletedConfig: c, } + + // load the configured path mappings + mappings, err := loadMappings(c.Options.MappingFile) + if err != nil { + return nil, fmt.Errorf("failed to load mappings from %q: %w", c.Options.MappingFile, err) + } + hasShardMapping := slices.ContainsFunc(mappings, isShardMapping) + + // When a mapping for shards is configured, we need to build up an index of which shard hosts + // which workspaces and logicalclusters. Ideally we'd start the index controller only if there + // is a shard mapping (i.e. a "/clusters/" entry in the mapping list), but there are other places + // in the general server package that always lookup the shard, so making the index optional would + // require deeper refactoring to make everything rely on the lookup middleware. + // If so, there will be also another middleware injected into the handler chain, which is responsible + // for resolving the incoming request and store the found cluster name in a context variable. rootShardConfigInformerClient, err := kcpclientset.NewForConfig(s.CompletedConfig.RootShardConfig) if err != nil { return s, fmt.Errorf("failed to create client for informers: %w", err) } s.KcpSharedInformerFactory = kcpinformers.NewSharedScopedInformerFactoryWithOptions(rootShardConfigInformerClient.Cluster(core.RootCluster.Path()), 30*time.Minute) - s.IndexController = index.NewController( - ctx, - s.KcpSharedInformerFactory.Core().V1alpha1().Shards(), - func(shard *corev1alpha1.Shard) (kcpclientset.ClusterInterface, error) { - shardConfig := restclient.CopyConfig(s.CompletedConfig.ShardsConfig) - shardConfig.Host = shard.Spec.BaseURL - shardClient, err := kcpclientset.NewForConfig(shardConfig) - if err != nil { - return nil, fmt.Errorf("failed to create shard %q client: %w", shard.Name, err) - } - return shardClient, nil - }, - ) - - handler, err := NewHandler(ctx, s.CompletedConfig.Options, s.IndexController) + + getClientFunc := func(shard *corev1alpha1.Shard) (kcpclientset.ClusterInterface, error) { + shardConfig := restclient.CopyConfig(s.CompletedConfig.ShardsConfig) + shardConfig.Host = shard.Spec.BaseURL + shardClient, err := kcpclientset.NewForConfig(shardConfig) + if err != nil { + return nil, fmt.Errorf("failed to create shard %q client: %w", shard.Name, err) + } + return shardClient, nil + } + + // This controller is responsible for watching all Shards, connecting to each of them and + // watching a number of resources on each. The controller is then also satisfying the Index + // interface. + s.IndexController = index.NewController(ctx, s.KcpSharedInformerFactory.Core().V1alpha1().Shards(), getClientFunc) + + handler, err := NewHandler(ctx, mappings, s.IndexController) if err != nil { return s, err } + // The optional auth handler will call the underlying authenticator only if + // auth methods are configured directly on the front-proxy *or* if there is + // a custom workspace authenticator, i.e. the AdditionalAuthEnabled field + // only represents the CLI flag state. failedHandler := frontproxyfilters.NewUnauthorizedHandler() handler = frontproxyfilters.WithOptionalAuthentication( handler, failedHandler, - s.CompletedConfig.AuthenticationInfo.Authenticator, + s.completedConfig.AuthenticationInfo.Authenticator, s.CompletedConfig.AdditionalAuthEnabled) + // Make the per-workspace authenticator available to the previous middleware + // by hooking up a handler and a runtime index. + hasWorkspaceAuth := hasShardMapping && kcpfeatures.DefaultFeatureGate.Enabled(kcpfeatures.WorkspaceAuthentication) + + if hasWorkspaceAuth { + // This controller is similar to the index controller, but keeps track of the per-workspace authenticators. + ctrl, err := authentication.NewController(ctx, s.KcpSharedInformerFactory.Core().V1alpha1().Shards(), getClientFunc, nil) + if err != nil { + return nil, fmt.Errorf("failed to start authentication controller: %w", err) + } + + s.AuthController = ctrl + + // When workspace auth is enabled, it depends on the target cluster whether + // a custom authenticator exists or not. This needs to be determined before + // the optionalAuthentication middleware can run, as it needs to know about + // the workspace authenticator. + handler = authentication.WithWorkspaceAuthResolver(handler, s.AuthController) + } + + if hasShardMapping { + // This middleware must happen before the authentication. + handler = lookup.WithClusterResolver(handler, s.IndexController) + } + requestInfoFactory := requestinfo.NewFactory() handler = kcpfilters.WithInClusterServiceAccountRequestRewrite(handler) handler = genericapifilters.WithRequestInfo(handler, requestInfoFactory) @@ -95,13 +146,11 @@ func NewServer(ctx context.Context, c CompletedConfig) (*Server, error) { // TODO: implement proper readyz handler mux.Handle("/readyz", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Write([]byte("OK")) //nolint:errcheck - w.WriteHeader(http.StatusOK) })) // TODO: implement proper livez handler mux.Handle("/livez", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Write([]byte("OK")) //nolint:errcheck - w.WriteHeader(http.StatusOK) })) mux.Handle("/", handler) @@ -132,9 +181,13 @@ func (s preparedServer) Run(ctx context.Context) error { return fmt.Errorf("failed to get or create identities: %w", err) } - // start index + // start indexes go s.IndexController.Start(ctx, 2) + if s.AuthController != nil { + go s.AuthController.Start(ctx, 2) + } + s.KcpSharedInformerFactory.Start(ctx.Done()) s.KcpSharedInformerFactory.WaitForCacheSync(ctx.Done()) diff --git a/pkg/server/config.go b/pkg/server/config.go index 5cc46f43eae..27f2d32fa08 100644 --- a/pkg/server/config.go +++ b/pkg/server/config.go @@ -31,6 +31,7 @@ import ( "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/util/sets" "k8s.io/apiserver/pkg/admission" + authenticatorunion "k8s.io/apiserver/pkg/authentication/request/union" "k8s.io/apiserver/pkg/endpoints/filters" "k8s.io/apiserver/pkg/informerfactoryhack" "k8s.io/apiserver/pkg/quota/v1/generic" @@ -57,8 +58,10 @@ import ( "github.com/kcp-dev/logicalcluster/v3" kcpadmissioninitializers "github.com/kcp-dev/kcp/pkg/admission/initializers" + "github.com/kcp-dev/kcp/pkg/authentication" "github.com/kcp-dev/kcp/pkg/authorization" bootstrappolicy "github.com/kcp-dev/kcp/pkg/authorization/bootstrap" + kcpfeatures "github.com/kcp-dev/kcp/pkg/features" "github.com/kcp-dev/kcp/pkg/informer" "github.com/kcp-dev/kcp/pkg/network" "github.com/kcp-dev/kcp/pkg/server/bootstrap" @@ -406,6 +409,22 @@ func NewConfig(ctx context.Context, opts kcpserveroptions.CompletedOptions) (*Co // Make sure to set our RequestInfoResolver that is capable of populating a RequestInfo even for /services/... URLs. c.GenericConfig.RequestInfoResolver = requestinfo.NewKCPRequestInfoResolver() + // Prepare an authentication index to be used later by a middleware. We start it early + // because it can potentially fail and the BuildHandlerChainFunc() has no way to return + // an error. + var authIndex authentication.AuthenticatorIndex + if kcpfeatures.DefaultFeatureGate.Enabled(kcpfeatures.WorkspaceAuthentication) { + // Start an index and a shard watcher to fill this index; + // the shard watcher's lifetime is tied to the given context. + authIndexState := authentication.NewIndex(ctx, c.GenericConfig.Authentication.APIAudiences) + _, err := authentication.NewShardWatcher(ctx, c.Options.Extra.ShardName, c.KcpClusterClient, authIndexState) + if err != nil { + return nil, fmt.Errorf("failed to start shard watcher: %w", err) + } + + authIndex = authIndexState + } + // preHandlerChainMux is called before the actual handler chain. Note that BuildHandlerChainFunc below // is called multiple times, but only one of the handler chain will actually be used. Hence, we wrap it // to give handlers below one mux.Handle func to call. @@ -457,6 +476,16 @@ func NewConfig(ctx context.Context, opts kcpserveroptions.CompletedOptions) (*Co apiHandler = WithVirtualWorkspacesProxy(apiHandler, shardVirtualWorkspaceURL, virtualWorkspaceServerProxyTransport, proxy) } + // Wrap authenticator with a per-workspace authenticator if desired. This authenticator + // requires the WorkspaceAuth middleware to have looked up and injected the relevant + // authenticator into the request context already. + if kcpfeatures.DefaultFeatureGate.Enabled(kcpfeatures.WorkspaceAuthentication) { + genericConfig.Authentication.Authenticator = authenticatorunion.New( + genericConfig.Authentication.Authenticator, + authentication.NewWorkspaceAuthenticator(), + ) + } + // There is ordering here in play: // 1. Default handlers up to impersonation gatekeeper preventing impersonation of the privileged user. // 2. Rest of the handlers up to Authz @@ -467,6 +496,14 @@ func NewConfig(ctx context.Context, opts kcpserveroptions.CompletedOptions) (*Co apiHandler = kcpfilters.WithImpersonationGatekeeper(apiHandler) apiHandler = genericapiserver.DefaultBuildHandlerChainFromStartToBeforeImpersonation(apiHandler, genericConfig) + // When workspace auth is enabled, it depends on the target cluster whether + // a custom authenticator exists or not. This needs to be determined before + // the authentication middleware can run, as it needs to know about the + // workspace authenticator. + if kcpfeatures.DefaultFeatureGate.Enabled(kcpfeatures.WorkspaceAuthentication) { + apiHandler = authentication.WithWorkspaceAuthResolver(apiHandler, authIndex) + } + // this will be replaced in DefaultBuildHandlerChain. So at worst we get twice as many warning. // But this is not harmful as the kcp warnings are not many. apiHandler = filters.WithWarningRecorder(apiHandler) diff --git a/pkg/server/localproxy.go b/pkg/server/localproxy.go index 699994b1cbe..3adedad3bf3 100644 --- a/pkg/server/localproxy.go +++ b/pkg/server/localproxy.go @@ -39,6 +39,7 @@ import ( "github.com/kcp-dev/kcp/pkg/index" indexrewriters "github.com/kcp-dev/kcp/pkg/index/rewriters" + "github.com/kcp-dev/kcp/pkg/proxy/lookup" "github.com/kcp-dev/kcp/pkg/server/filters" "github.com/kcp-dev/kcp/pkg/server/proxy" corev1alpha1 "github.com/kcp-dev/kcp/sdk/apis/core/v1alpha1" @@ -198,6 +199,8 @@ func WithLocalProxy( cluster.Name = clusterName } ctx = request.WithCluster(ctx, cluster) + ctx = lookup.WithClusterName(ctx, cluster.Name) + ctx = lookup.WithWorkspaceType(ctx, r.Type) handler.ServeHTTP(w, req.WithContext(ctx)) }) diff --git a/pkg/server/proxy/handler.go b/pkg/server/proxy/handler.go index be5ca366123..52a3649d9bb 100644 --- a/pkg/server/proxy/handler.go +++ b/pkg/server/proxy/handler.go @@ -83,12 +83,13 @@ func (h *HttpHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { // use the handler for that mapping, else use default handler - kcp. // Mappings are done from most specific to least specific: // Example: /clusters/cluster1/ will be matched before /clusters/ . + url, errorCode := h.resolveURL(r) + if errorCode != 0 { + http.Error(w, http.StatusText(errorCode), errorCode) + return + } + for _, m := range h.Mappings { - url, errorCode := h.resolveURL(r) - if errorCode != 0 { - http.Error(w, http.StatusText(errorCode), errorCode) - return - } if strings.HasPrefix(url, m.Path) { m.Handler.ServeHTTP(w, r) return diff --git a/sdk/apis/tenancy/v1alpha1/register.go b/sdk/apis/tenancy/v1alpha1/register.go index 9d9a4631e29..66ff2f5b53e 100644 --- a/sdk/apis/tenancy/v1alpha1/register.go +++ b/sdk/apis/tenancy/v1alpha1/register.go @@ -49,6 +49,8 @@ func addKnownTypes(scheme *runtime.Scheme) error { &WorkspaceList{}, &WorkspaceType{}, &WorkspaceTypeList{}, + &WorkspaceAuthenticationConfiguration{}, + &WorkspaceAuthenticationConfigurationList{}, ) metav1.AddToGroupVersion(scheme, SchemeGroupVersion) return nil diff --git a/sdk/apis/tenancy/v1alpha1/types_workspaceauthentication.go b/sdk/apis/tenancy/v1alpha1/types_workspaceauthentication.go new file mode 100644 index 00000000000..c34c1d4eda2 --- /dev/null +++ b/sdk/apis/tenancy/v1alpha1/types_workspaceauthentication.go @@ -0,0 +1,159 @@ +/* +Copyright 2025 The KCP Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v1alpha1 + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// WorkspaceAuthenticationConfiguration specifies additional authentication options +// for workspaces. +// +// +crd +// +genclient +// +genclient:nonNamespaced +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object +// +kubebuilder:resource:scope=Cluster,categories=kcp +type WorkspaceAuthenticationConfiguration struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata"` + + Spec WorkspaceAuthenticationConfigurationSpec `json:"spec"` +} + +type WorkspaceAuthenticationConfigurationSpec struct { + JWT []JWTAuthenticator `json:"jwt"` +} + +type JWTAuthenticator struct { + Issuer Issuer `json:"issuer"` + // +optional + ClaimValidationRules []ClaimValidationRule `json:"claimValidationRules,omitempty"` + ClaimMappings ClaimMappings `json:"claimMappings"` + // +optional + UserValidationRules []UserValidationRule `json:"userValidationRules,omitempty"` +} + +// Issuer provides the configuration for an external provider's specific settings. +type Issuer struct { + // url points to the issuer URL in a format https://url or https://url/path. + // This must match the "iss" claim in the presented JWT, and the issuer returned from discovery. + // Same value as the --oidc-issuer-url flag. + // Discovery information is fetched from "{url}/.well-known/openid-configuration" unless overridden by discoveryURL. + // Required to be unique across all JWT authenticators. + // Note that egress selection configuration is not used for this network connection. + // +required + URL string `json:"url"` + // discoveryURL, if specified, overrides the URL used to fetch discovery + // information instead of using "{url}/.well-known/openid-configuration". + // The exact value specified is used, so "/.well-known/openid-configuration" + // must be included in discoveryURL if needed. + // + // The "issuer" field in the fetched discovery information must match the "issuer.url" field + // in the AuthenticationConfiguration and will be used to validate the "iss" claim in the presented JWT. + // This is for scenarios where the well-known and jwks endpoints are hosted at a different + // location than the issuer (such as locally in the cluster). + // + // Example: + // A discovery url that is exposed using kubernetes service 'oidc' in namespace 'oidc-namespace' + // and discovery information is available at '/.well-known/openid-configuration'. + // discoveryURL: "https://oidc.oidc-namespace/.well-known/openid-configuration" + // certificateAuthority is used to verify the TLS connection and the hostname on the leaf certificate + // must be set to 'oidc.oidc-namespace'. + // + // curl https://oidc.oidc-namespace/.well-known/openid-configuration (.discoveryURL field) + // { + // issuer: "https://oidc.example.com" (.url field) + // } + // + // discoveryURL must be different from url. + // Required to be unique across all JWT authenticators. + // Note that egress selection configuration is not used for this network connection. + // +optional + DiscoveryURL string `json:"discoveryURL,omitempty"` + // +optional + CertificateAuthority string `json:"certificateAuthority,omitempty"` + // +optional + Audiences []string `json:"audiences,omitempty"` + // +optional + AudienceMatchPolicy AudienceMatchPolicyType `json:"audienceMatchPolicy,omitempty"` +} + +// AudienceMatchPolicyType is a set of valid values for Issuer.AudienceMatchPolicy. +type AudienceMatchPolicyType string + +// Valid types for AudienceMatchPolicyType. +const ( + AudienceMatchPolicyMatchAny AudienceMatchPolicyType = "MatchAny" +) + +// ClaimValidationRule provides the configuration for a single claim validation rule. +type ClaimValidationRule struct { + Claim string `json:"claim"` + RequiredValue string `json:"requiredValue"` + + Expression string `json:"expression"` + Message string `json:"message"` +} + +// ClaimMappings provides the configuration for claim mapping. +type ClaimMappings struct { + Username PrefixedClaimOrExpression `json:"username"` + Groups PrefixedClaimOrExpression `json:"groups"` + // +optional + UID ClaimOrExpression `json:"uid,omitempty"` + // +optional + Extra []ExtraMapping `json:"extra,omitempty"` +} + +// PrefixedClaimOrExpression provides the configuration for a single prefixed claim or expression. +type PrefixedClaimOrExpression struct { + Claim string `json:"claim"` + // +optional + Prefix *string `json:"prefix,omitempty"` + // +optional + Expression string `json:"expression,omitempty"` +} + +// ClaimOrExpression provides the configuration for a single claim or expression. +type ClaimOrExpression struct { + Claim string `json:"claim"` + // +optional + Expression string `json:"expression,omitempty"` +} + +// ExtraMapping provides the configuration for a single extra mapping. +type ExtraMapping struct { + Key string `json:"key"` + ValueExpression string `json:"valueExpression"` +} + +// UserValidationRule provides the configuration for a single user validation rule. +type UserValidationRule struct { + Expression string `json:"expression"` + Message string `json:"message"` +} + +// WorkspaceAuthenticationConfigurationList is a list of WorkspaceAuthenticationConfigurations. +// +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object +type WorkspaceAuthenticationConfigurationList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata"` + + Items []WorkspaceAuthenticationConfiguration `json:"items"` +} diff --git a/sdk/apis/tenancy/v1alpha1/types_workspacetype.go b/sdk/apis/tenancy/v1alpha1/types_workspacetype.go index eac01f48809..21e9be2e380 100644 --- a/sdk/apis/tenancy/v1alpha1/types_workspacetype.go +++ b/sdk/apis/tenancy/v1alpha1/types_workspacetype.go @@ -127,6 +127,12 @@ type WorkspaceTypeSpec struct { // +optional // +kubebuilder:validation:Enum=InitializeOnly;Maintain DefaultAPIBindingLifecycle *APIBindingLifecycleMode `json:"defaultAPIBindingLifecycle,omitempty"` + + // authenticationConfigurations are additional authentication options that should apply to any + // workspace using this workspace type. + // + // +optional + AuthenticationConfigurations []AuthenticationConfigurationReference `json:"authenticationConfigurations,omitempty"` } // APIExportReference provides the fields necessary to resolve an APIExport. @@ -147,6 +153,16 @@ type APIExportReference struct { Export string `json:"export"` } +// AuthenticationConfigurationReference provides the fields necessary to resolve a WorkspaceAuthenticationConfiguration. +type AuthenticationConfigurationReference struct { + // name is the name of the WorkspaceAuthenticationConfiguration. + // + // +required + // +kubebuilder:validation:Required + // +kube:validation:MinLength=1 + Name string `json:"name"` +} + // APIBindingLifecycleMode defines how the lifecycle of an APIBinding is // managed. type APIBindingLifecycleMode string diff --git a/sdk/apis/tenancy/v1alpha1/zz_generated.deepcopy.go b/sdk/apis/tenancy/v1alpha1/zz_generated.deepcopy.go index fa107b2f7d4..d3bf3f7d9ad 100644 --- a/sdk/apis/tenancy/v1alpha1/zz_generated.deepcopy.go +++ b/sdk/apis/tenancy/v1alpha1/zz_generated.deepcopy.go @@ -45,6 +45,143 @@ func (in *APIExportReference) DeepCopy() *APIExportReference { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *AuthenticationConfigurationReference) DeepCopyInto(out *AuthenticationConfigurationReference) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AuthenticationConfigurationReference. +func (in *AuthenticationConfigurationReference) DeepCopy() *AuthenticationConfigurationReference { + if in == nil { + return nil + } + out := new(AuthenticationConfigurationReference) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ClaimMappings) DeepCopyInto(out *ClaimMappings) { + *out = *in + in.Username.DeepCopyInto(&out.Username) + in.Groups.DeepCopyInto(&out.Groups) + out.UID = in.UID + if in.Extra != nil { + in, out := &in.Extra, &out.Extra + *out = make([]ExtraMapping, len(*in)) + copy(*out, *in) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ClaimMappings. +func (in *ClaimMappings) DeepCopy() *ClaimMappings { + if in == nil { + return nil + } + out := new(ClaimMappings) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ClaimOrExpression) DeepCopyInto(out *ClaimOrExpression) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ClaimOrExpression. +func (in *ClaimOrExpression) DeepCopy() *ClaimOrExpression { + if in == nil { + return nil + } + out := new(ClaimOrExpression) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ClaimValidationRule) DeepCopyInto(out *ClaimValidationRule) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ClaimValidationRule. +func (in *ClaimValidationRule) DeepCopy() *ClaimValidationRule { + if in == nil { + return nil + } + out := new(ClaimValidationRule) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ExtraMapping) DeepCopyInto(out *ExtraMapping) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ExtraMapping. +func (in *ExtraMapping) DeepCopy() *ExtraMapping { + if in == nil { + return nil + } + out := new(ExtraMapping) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Issuer) DeepCopyInto(out *Issuer) { + *out = *in + if in.Audiences != nil { + in, out := &in.Audiences, &out.Audiences + *out = make([]string, len(*in)) + copy(*out, *in) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Issuer. +func (in *Issuer) DeepCopy() *Issuer { + if in == nil { + return nil + } + out := new(Issuer) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *JWTAuthenticator) DeepCopyInto(out *JWTAuthenticator) { + *out = *in + in.Issuer.DeepCopyInto(&out.Issuer) + if in.ClaimValidationRules != nil { + in, out := &in.ClaimValidationRules, &out.ClaimValidationRules + *out = make([]ClaimValidationRule, len(*in)) + copy(*out, *in) + } + in.ClaimMappings.DeepCopyInto(&out.ClaimMappings) + if in.UserValidationRules != nil { + in, out := &in.UserValidationRules, &out.UserValidationRules + *out = make([]UserValidationRule, len(*in)) + copy(*out, *in) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new JWTAuthenticator. +func (in *JWTAuthenticator) DeepCopy() *JWTAuthenticator { + if in == nil { + return nil + } + out := new(JWTAuthenticator) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *Mount) DeepCopyInto(out *Mount) { *out = *in @@ -78,6 +215,43 @@ func (in *ObjectReference) DeepCopy() *ObjectReference { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *PrefixedClaimOrExpression) DeepCopyInto(out *PrefixedClaimOrExpression) { + *out = *in + if in.Prefix != nil { + in, out := &in.Prefix, &out.Prefix + *out = new(string) + **out = **in + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PrefixedClaimOrExpression. +func (in *PrefixedClaimOrExpression) DeepCopy() *PrefixedClaimOrExpression { + if in == nil { + return nil + } + out := new(PrefixedClaimOrExpression) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *UserValidationRule) DeepCopyInto(out *UserValidationRule) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new UserValidationRule. +func (in *UserValidationRule) DeepCopy() *UserValidationRule { + if in == nil { + return nil + } + out := new(UserValidationRule) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *VirtualWorkspace) DeepCopyInto(out *VirtualWorkspace) { *out = *in @@ -122,6 +296,89 @@ func (in *Workspace) DeepCopyObject() runtime.Object { return nil } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *WorkspaceAuthenticationConfiguration) DeepCopyInto(out *WorkspaceAuthenticationConfiguration) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new WorkspaceAuthenticationConfiguration. +func (in *WorkspaceAuthenticationConfiguration) DeepCopy() *WorkspaceAuthenticationConfiguration { + if in == nil { + return nil + } + out := new(WorkspaceAuthenticationConfiguration) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *WorkspaceAuthenticationConfiguration) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *WorkspaceAuthenticationConfigurationList) DeepCopyInto(out *WorkspaceAuthenticationConfigurationList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]WorkspaceAuthenticationConfiguration, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new WorkspaceAuthenticationConfigurationList. +func (in *WorkspaceAuthenticationConfigurationList) DeepCopy() *WorkspaceAuthenticationConfigurationList { + if in == nil { + return nil + } + out := new(WorkspaceAuthenticationConfigurationList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *WorkspaceAuthenticationConfigurationList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *WorkspaceAuthenticationConfigurationSpec) DeepCopyInto(out *WorkspaceAuthenticationConfigurationSpec) { + *out = *in + if in.JWT != nil { + in, out := &in.JWT, &out.JWT + *out = make([]JWTAuthenticator, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new WorkspaceAuthenticationConfigurationSpec. +func (in *WorkspaceAuthenticationConfigurationSpec) DeepCopy() *WorkspaceAuthenticationConfigurationSpec { + if in == nil { + return nil + } + out := new(WorkspaceAuthenticationConfigurationSpec) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *WorkspaceList) DeepCopyInto(out *WorkspaceList) { *out = *in @@ -390,6 +647,11 @@ func (in *WorkspaceTypeSpec) DeepCopyInto(out *WorkspaceTypeSpec) { *out = new(APIBindingLifecycleMode) **out = **in } + if in.AuthenticationConfigurations != nil { + in, out := &in.AuthenticationConfigurations, &out.AuthenticationConfigurations + *out = make([]AuthenticationConfigurationReference, len(*in)) + copy(*out, *in) + } return } diff --git a/sdk/client/applyconfiguration/tenancy/v1alpha1/authenticationconfigurationreference.go b/sdk/client/applyconfiguration/tenancy/v1alpha1/authenticationconfigurationreference.go new file mode 100644 index 00000000000..41a80c8b403 --- /dev/null +++ b/sdk/client/applyconfiguration/tenancy/v1alpha1/authenticationconfigurationreference.go @@ -0,0 +1,39 @@ +/* +Copyright The KCP Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Code generated by applyconfiguration-gen. DO NOT EDIT. + +package v1alpha1 + +// AuthenticationConfigurationReferenceApplyConfiguration represents a declarative configuration of the AuthenticationConfigurationReference type for use +// with apply. +type AuthenticationConfigurationReferenceApplyConfiguration struct { + Name *string `json:"name,omitempty"` +} + +// AuthenticationConfigurationReferenceApplyConfiguration constructs a declarative configuration of the AuthenticationConfigurationReference type for use with +// apply. +func AuthenticationConfigurationReference() *AuthenticationConfigurationReferenceApplyConfiguration { + return &AuthenticationConfigurationReferenceApplyConfiguration{} +} + +// WithName sets the Name field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the Name field is set to the value of the last call. +func (b *AuthenticationConfigurationReferenceApplyConfiguration) WithName(value string) *AuthenticationConfigurationReferenceApplyConfiguration { + b.Name = &value + return b +} diff --git a/sdk/client/applyconfiguration/tenancy/v1alpha1/claimmappings.go b/sdk/client/applyconfiguration/tenancy/v1alpha1/claimmappings.go new file mode 100644 index 00000000000..a6ca21efdaa --- /dev/null +++ b/sdk/client/applyconfiguration/tenancy/v1alpha1/claimmappings.go @@ -0,0 +1,71 @@ +/* +Copyright The KCP Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Code generated by applyconfiguration-gen. DO NOT EDIT. + +package v1alpha1 + +// ClaimMappingsApplyConfiguration represents a declarative configuration of the ClaimMappings type for use +// with apply. +type ClaimMappingsApplyConfiguration struct { + Username *PrefixedClaimOrExpressionApplyConfiguration `json:"username,omitempty"` + Groups *PrefixedClaimOrExpressionApplyConfiguration `json:"groups,omitempty"` + UID *ClaimOrExpressionApplyConfiguration `json:"uid,omitempty"` + Extra []ExtraMappingApplyConfiguration `json:"extra,omitempty"` +} + +// ClaimMappingsApplyConfiguration constructs a declarative configuration of the ClaimMappings type for use with +// apply. +func ClaimMappings() *ClaimMappingsApplyConfiguration { + return &ClaimMappingsApplyConfiguration{} +} + +// WithUsername sets the Username field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the Username field is set to the value of the last call. +func (b *ClaimMappingsApplyConfiguration) WithUsername(value *PrefixedClaimOrExpressionApplyConfiguration) *ClaimMappingsApplyConfiguration { + b.Username = value + return b +} + +// WithGroups sets the Groups field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the Groups field is set to the value of the last call. +func (b *ClaimMappingsApplyConfiguration) WithGroups(value *PrefixedClaimOrExpressionApplyConfiguration) *ClaimMappingsApplyConfiguration { + b.Groups = value + return b +} + +// WithUID sets the UID field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the UID field is set to the value of the last call. +func (b *ClaimMappingsApplyConfiguration) WithUID(value *ClaimOrExpressionApplyConfiguration) *ClaimMappingsApplyConfiguration { + b.UID = value + return b +} + +// WithExtra adds the given value to the Extra field in the declarative configuration +// and returns the receiver, so that objects can be build by chaining "With" function invocations. +// If called multiple times, values provided by each call will be appended to the Extra field. +func (b *ClaimMappingsApplyConfiguration) WithExtra(values ...*ExtraMappingApplyConfiguration) *ClaimMappingsApplyConfiguration { + for i := range values { + if values[i] == nil { + panic("nil value passed to WithExtra") + } + b.Extra = append(b.Extra, *values[i]) + } + return b +} diff --git a/sdk/client/applyconfiguration/tenancy/v1alpha1/claimorexpression.go b/sdk/client/applyconfiguration/tenancy/v1alpha1/claimorexpression.go new file mode 100644 index 00000000000..8a074ccc918 --- /dev/null +++ b/sdk/client/applyconfiguration/tenancy/v1alpha1/claimorexpression.go @@ -0,0 +1,48 @@ +/* +Copyright The KCP Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Code generated by applyconfiguration-gen. DO NOT EDIT. + +package v1alpha1 + +// ClaimOrExpressionApplyConfiguration represents a declarative configuration of the ClaimOrExpression type for use +// with apply. +type ClaimOrExpressionApplyConfiguration struct { + Claim *string `json:"claim,omitempty"` + Expression *string `json:"expression,omitempty"` +} + +// ClaimOrExpressionApplyConfiguration constructs a declarative configuration of the ClaimOrExpression type for use with +// apply. +func ClaimOrExpression() *ClaimOrExpressionApplyConfiguration { + return &ClaimOrExpressionApplyConfiguration{} +} + +// WithClaim sets the Claim field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the Claim field is set to the value of the last call. +func (b *ClaimOrExpressionApplyConfiguration) WithClaim(value string) *ClaimOrExpressionApplyConfiguration { + b.Claim = &value + return b +} + +// WithExpression sets the Expression field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the Expression field is set to the value of the last call. +func (b *ClaimOrExpressionApplyConfiguration) WithExpression(value string) *ClaimOrExpressionApplyConfiguration { + b.Expression = &value + return b +} diff --git a/sdk/client/applyconfiguration/tenancy/v1alpha1/claimvalidationrule.go b/sdk/client/applyconfiguration/tenancy/v1alpha1/claimvalidationrule.go new file mode 100644 index 00000000000..5473edd1b29 --- /dev/null +++ b/sdk/client/applyconfiguration/tenancy/v1alpha1/claimvalidationrule.go @@ -0,0 +1,66 @@ +/* +Copyright The KCP Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Code generated by applyconfiguration-gen. DO NOT EDIT. + +package v1alpha1 + +// ClaimValidationRuleApplyConfiguration represents a declarative configuration of the ClaimValidationRule type for use +// with apply. +type ClaimValidationRuleApplyConfiguration struct { + Claim *string `json:"claim,omitempty"` + RequiredValue *string `json:"requiredValue,omitempty"` + Expression *string `json:"expression,omitempty"` + Message *string `json:"message,omitempty"` +} + +// ClaimValidationRuleApplyConfiguration constructs a declarative configuration of the ClaimValidationRule type for use with +// apply. +func ClaimValidationRule() *ClaimValidationRuleApplyConfiguration { + return &ClaimValidationRuleApplyConfiguration{} +} + +// WithClaim sets the Claim field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the Claim field is set to the value of the last call. +func (b *ClaimValidationRuleApplyConfiguration) WithClaim(value string) *ClaimValidationRuleApplyConfiguration { + b.Claim = &value + return b +} + +// WithRequiredValue sets the RequiredValue field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the RequiredValue field is set to the value of the last call. +func (b *ClaimValidationRuleApplyConfiguration) WithRequiredValue(value string) *ClaimValidationRuleApplyConfiguration { + b.RequiredValue = &value + return b +} + +// WithExpression sets the Expression field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the Expression field is set to the value of the last call. +func (b *ClaimValidationRuleApplyConfiguration) WithExpression(value string) *ClaimValidationRuleApplyConfiguration { + b.Expression = &value + return b +} + +// WithMessage sets the Message field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the Message field is set to the value of the last call. +func (b *ClaimValidationRuleApplyConfiguration) WithMessage(value string) *ClaimValidationRuleApplyConfiguration { + b.Message = &value + return b +} diff --git a/sdk/client/applyconfiguration/tenancy/v1alpha1/extramapping.go b/sdk/client/applyconfiguration/tenancy/v1alpha1/extramapping.go new file mode 100644 index 00000000000..a9a82205584 --- /dev/null +++ b/sdk/client/applyconfiguration/tenancy/v1alpha1/extramapping.go @@ -0,0 +1,48 @@ +/* +Copyright The KCP Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Code generated by applyconfiguration-gen. DO NOT EDIT. + +package v1alpha1 + +// ExtraMappingApplyConfiguration represents a declarative configuration of the ExtraMapping type for use +// with apply. +type ExtraMappingApplyConfiguration struct { + Key *string `json:"key,omitempty"` + ValueExpression *string `json:"valueExpression,omitempty"` +} + +// ExtraMappingApplyConfiguration constructs a declarative configuration of the ExtraMapping type for use with +// apply. +func ExtraMapping() *ExtraMappingApplyConfiguration { + return &ExtraMappingApplyConfiguration{} +} + +// WithKey sets the Key field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the Key field is set to the value of the last call. +func (b *ExtraMappingApplyConfiguration) WithKey(value string) *ExtraMappingApplyConfiguration { + b.Key = &value + return b +} + +// WithValueExpression sets the ValueExpression field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the ValueExpression field is set to the value of the last call. +func (b *ExtraMappingApplyConfiguration) WithValueExpression(value string) *ExtraMappingApplyConfiguration { + b.ValueExpression = &value + return b +} diff --git a/sdk/client/applyconfiguration/tenancy/v1alpha1/issuer.go b/sdk/client/applyconfiguration/tenancy/v1alpha1/issuer.go new file mode 100644 index 00000000000..a679a950811 --- /dev/null +++ b/sdk/client/applyconfiguration/tenancy/v1alpha1/issuer.go @@ -0,0 +1,81 @@ +/* +Copyright The KCP Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Code generated by applyconfiguration-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + tenancyv1alpha1 "github.com/kcp-dev/kcp/sdk/apis/tenancy/v1alpha1" +) + +// IssuerApplyConfiguration represents a declarative configuration of the Issuer type for use +// with apply. +type IssuerApplyConfiguration struct { + URL *string `json:"url,omitempty"` + DiscoveryURL *string `json:"discoveryURL,omitempty"` + CertificateAuthority *string `json:"certificateAuthority,omitempty"` + Audiences []string `json:"audiences,omitempty"` + AudienceMatchPolicy *tenancyv1alpha1.AudienceMatchPolicyType `json:"audienceMatchPolicy,omitempty"` +} + +// IssuerApplyConfiguration constructs a declarative configuration of the Issuer type for use with +// apply. +func Issuer() *IssuerApplyConfiguration { + return &IssuerApplyConfiguration{} +} + +// WithURL sets the URL field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the URL field is set to the value of the last call. +func (b *IssuerApplyConfiguration) WithURL(value string) *IssuerApplyConfiguration { + b.URL = &value + return b +} + +// WithDiscoveryURL sets the DiscoveryURL field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the DiscoveryURL field is set to the value of the last call. +func (b *IssuerApplyConfiguration) WithDiscoveryURL(value string) *IssuerApplyConfiguration { + b.DiscoveryURL = &value + return b +} + +// WithCertificateAuthority sets the CertificateAuthority field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the CertificateAuthority field is set to the value of the last call. +func (b *IssuerApplyConfiguration) WithCertificateAuthority(value string) *IssuerApplyConfiguration { + b.CertificateAuthority = &value + return b +} + +// WithAudiences adds the given value to the Audiences field in the declarative configuration +// and returns the receiver, so that objects can be build by chaining "With" function invocations. +// If called multiple times, values provided by each call will be appended to the Audiences field. +func (b *IssuerApplyConfiguration) WithAudiences(values ...string) *IssuerApplyConfiguration { + for i := range values { + b.Audiences = append(b.Audiences, values[i]) + } + return b +} + +// WithAudienceMatchPolicy sets the AudienceMatchPolicy field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the AudienceMatchPolicy field is set to the value of the last call. +func (b *IssuerApplyConfiguration) WithAudienceMatchPolicy(value tenancyv1alpha1.AudienceMatchPolicyType) *IssuerApplyConfiguration { + b.AudienceMatchPolicy = &value + return b +} diff --git a/sdk/client/applyconfiguration/tenancy/v1alpha1/jwtauthenticator.go b/sdk/client/applyconfiguration/tenancy/v1alpha1/jwtauthenticator.go new file mode 100644 index 00000000000..153169c431e --- /dev/null +++ b/sdk/client/applyconfiguration/tenancy/v1alpha1/jwtauthenticator.go @@ -0,0 +1,76 @@ +/* +Copyright The KCP Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Code generated by applyconfiguration-gen. DO NOT EDIT. + +package v1alpha1 + +// JWTAuthenticatorApplyConfiguration represents a declarative configuration of the JWTAuthenticator type for use +// with apply. +type JWTAuthenticatorApplyConfiguration struct { + Issuer *IssuerApplyConfiguration `json:"issuer,omitempty"` + ClaimValidationRules []ClaimValidationRuleApplyConfiguration `json:"claimValidationRules,omitempty"` + ClaimMappings *ClaimMappingsApplyConfiguration `json:"claimMappings,omitempty"` + UserValidationRules []UserValidationRuleApplyConfiguration `json:"userValidationRules,omitempty"` +} + +// JWTAuthenticatorApplyConfiguration constructs a declarative configuration of the JWTAuthenticator type for use with +// apply. +func JWTAuthenticator() *JWTAuthenticatorApplyConfiguration { + return &JWTAuthenticatorApplyConfiguration{} +} + +// WithIssuer sets the Issuer field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the Issuer field is set to the value of the last call. +func (b *JWTAuthenticatorApplyConfiguration) WithIssuer(value *IssuerApplyConfiguration) *JWTAuthenticatorApplyConfiguration { + b.Issuer = value + return b +} + +// WithClaimValidationRules adds the given value to the ClaimValidationRules field in the declarative configuration +// and returns the receiver, so that objects can be build by chaining "With" function invocations. +// If called multiple times, values provided by each call will be appended to the ClaimValidationRules field. +func (b *JWTAuthenticatorApplyConfiguration) WithClaimValidationRules(values ...*ClaimValidationRuleApplyConfiguration) *JWTAuthenticatorApplyConfiguration { + for i := range values { + if values[i] == nil { + panic("nil value passed to WithClaimValidationRules") + } + b.ClaimValidationRules = append(b.ClaimValidationRules, *values[i]) + } + return b +} + +// WithClaimMappings sets the ClaimMappings field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the ClaimMappings field is set to the value of the last call. +func (b *JWTAuthenticatorApplyConfiguration) WithClaimMappings(value *ClaimMappingsApplyConfiguration) *JWTAuthenticatorApplyConfiguration { + b.ClaimMappings = value + return b +} + +// WithUserValidationRules adds the given value to the UserValidationRules field in the declarative configuration +// and returns the receiver, so that objects can be build by chaining "With" function invocations. +// If called multiple times, values provided by each call will be appended to the UserValidationRules field. +func (b *JWTAuthenticatorApplyConfiguration) WithUserValidationRules(values ...*UserValidationRuleApplyConfiguration) *JWTAuthenticatorApplyConfiguration { + for i := range values { + if values[i] == nil { + panic("nil value passed to WithUserValidationRules") + } + b.UserValidationRules = append(b.UserValidationRules, *values[i]) + } + return b +} diff --git a/sdk/client/applyconfiguration/tenancy/v1alpha1/prefixedclaimorexpression.go b/sdk/client/applyconfiguration/tenancy/v1alpha1/prefixedclaimorexpression.go new file mode 100644 index 00000000000..5d3759c0839 --- /dev/null +++ b/sdk/client/applyconfiguration/tenancy/v1alpha1/prefixedclaimorexpression.go @@ -0,0 +1,57 @@ +/* +Copyright The KCP Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Code generated by applyconfiguration-gen. DO NOT EDIT. + +package v1alpha1 + +// PrefixedClaimOrExpressionApplyConfiguration represents a declarative configuration of the PrefixedClaimOrExpression type for use +// with apply. +type PrefixedClaimOrExpressionApplyConfiguration struct { + Claim *string `json:"claim,omitempty"` + Prefix *string `json:"prefix,omitempty"` + Expression *string `json:"expression,omitempty"` +} + +// PrefixedClaimOrExpressionApplyConfiguration constructs a declarative configuration of the PrefixedClaimOrExpression type for use with +// apply. +func PrefixedClaimOrExpression() *PrefixedClaimOrExpressionApplyConfiguration { + return &PrefixedClaimOrExpressionApplyConfiguration{} +} + +// WithClaim sets the Claim field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the Claim field is set to the value of the last call. +func (b *PrefixedClaimOrExpressionApplyConfiguration) WithClaim(value string) *PrefixedClaimOrExpressionApplyConfiguration { + b.Claim = &value + return b +} + +// WithPrefix sets the Prefix field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the Prefix field is set to the value of the last call. +func (b *PrefixedClaimOrExpressionApplyConfiguration) WithPrefix(value string) *PrefixedClaimOrExpressionApplyConfiguration { + b.Prefix = &value + return b +} + +// WithExpression sets the Expression field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the Expression field is set to the value of the last call. +func (b *PrefixedClaimOrExpressionApplyConfiguration) WithExpression(value string) *PrefixedClaimOrExpressionApplyConfiguration { + b.Expression = &value + return b +} diff --git a/sdk/client/applyconfiguration/tenancy/v1alpha1/uservalidationrule.go b/sdk/client/applyconfiguration/tenancy/v1alpha1/uservalidationrule.go new file mode 100644 index 00000000000..fe72776d0be --- /dev/null +++ b/sdk/client/applyconfiguration/tenancy/v1alpha1/uservalidationrule.go @@ -0,0 +1,48 @@ +/* +Copyright The KCP Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Code generated by applyconfiguration-gen. DO NOT EDIT. + +package v1alpha1 + +// UserValidationRuleApplyConfiguration represents a declarative configuration of the UserValidationRule type for use +// with apply. +type UserValidationRuleApplyConfiguration struct { + Expression *string `json:"expression,omitempty"` + Message *string `json:"message,omitempty"` +} + +// UserValidationRuleApplyConfiguration constructs a declarative configuration of the UserValidationRule type for use with +// apply. +func UserValidationRule() *UserValidationRuleApplyConfiguration { + return &UserValidationRuleApplyConfiguration{} +} + +// WithExpression sets the Expression field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the Expression field is set to the value of the last call. +func (b *UserValidationRuleApplyConfiguration) WithExpression(value string) *UserValidationRuleApplyConfiguration { + b.Expression = &value + return b +} + +// WithMessage sets the Message field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the Message field is set to the value of the last call. +func (b *UserValidationRuleApplyConfiguration) WithMessage(value string) *UserValidationRuleApplyConfiguration { + b.Message = &value + return b +} diff --git a/sdk/client/applyconfiguration/tenancy/v1alpha1/workspaceauthenticationconfiguration.go b/sdk/client/applyconfiguration/tenancy/v1alpha1/workspaceauthenticationconfiguration.go new file mode 100644 index 00000000000..24c54cc02ab --- /dev/null +++ b/sdk/client/applyconfiguration/tenancy/v1alpha1/workspaceauthenticationconfiguration.go @@ -0,0 +1,216 @@ +/* +Copyright The KCP Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Code generated by applyconfiguration-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + types "k8s.io/apimachinery/pkg/types" + + v1 "github.com/kcp-dev/kcp/sdk/client/applyconfiguration/meta/v1" +) + +// WorkspaceAuthenticationConfigurationApplyConfiguration represents a declarative configuration of the WorkspaceAuthenticationConfiguration type for use +// with apply. +type WorkspaceAuthenticationConfigurationApplyConfiguration struct { + v1.TypeMetaApplyConfiguration `json:",inline"` + *v1.ObjectMetaApplyConfiguration `json:"metadata,omitempty"` + Spec *WorkspaceAuthenticationConfigurationSpecApplyConfiguration `json:"spec,omitempty"` +} + +// WorkspaceAuthenticationConfiguration constructs a declarative configuration of the WorkspaceAuthenticationConfiguration type for use with +// apply. +func WorkspaceAuthenticationConfiguration(name string) *WorkspaceAuthenticationConfigurationApplyConfiguration { + b := &WorkspaceAuthenticationConfigurationApplyConfiguration{} + b.WithName(name) + b.WithKind("WorkspaceAuthenticationConfiguration") + b.WithAPIVersion("tenancy.kcp.io/v1alpha1") + return b +} + +// WithKind sets the Kind field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the Kind field is set to the value of the last call. +func (b *WorkspaceAuthenticationConfigurationApplyConfiguration) WithKind(value string) *WorkspaceAuthenticationConfigurationApplyConfiguration { + b.TypeMetaApplyConfiguration.Kind = &value + return b +} + +// WithAPIVersion sets the APIVersion field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the APIVersion field is set to the value of the last call. +func (b *WorkspaceAuthenticationConfigurationApplyConfiguration) WithAPIVersion(value string) *WorkspaceAuthenticationConfigurationApplyConfiguration { + b.TypeMetaApplyConfiguration.APIVersion = &value + return b +} + +// WithName sets the Name field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the Name field is set to the value of the last call. +func (b *WorkspaceAuthenticationConfigurationApplyConfiguration) WithName(value string) *WorkspaceAuthenticationConfigurationApplyConfiguration { + b.ensureObjectMetaApplyConfigurationExists() + b.ObjectMetaApplyConfiguration.Name = &value + return b +} + +// WithGenerateName sets the GenerateName field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the GenerateName field is set to the value of the last call. +func (b *WorkspaceAuthenticationConfigurationApplyConfiguration) WithGenerateName(value string) *WorkspaceAuthenticationConfigurationApplyConfiguration { + b.ensureObjectMetaApplyConfigurationExists() + b.ObjectMetaApplyConfiguration.GenerateName = &value + return b +} + +// WithNamespace sets the Namespace field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the Namespace field is set to the value of the last call. +func (b *WorkspaceAuthenticationConfigurationApplyConfiguration) WithNamespace(value string) *WorkspaceAuthenticationConfigurationApplyConfiguration { + b.ensureObjectMetaApplyConfigurationExists() + b.ObjectMetaApplyConfiguration.Namespace = &value + return b +} + +// WithUID sets the UID field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the UID field is set to the value of the last call. +func (b *WorkspaceAuthenticationConfigurationApplyConfiguration) WithUID(value types.UID) *WorkspaceAuthenticationConfigurationApplyConfiguration { + b.ensureObjectMetaApplyConfigurationExists() + b.ObjectMetaApplyConfiguration.UID = &value + return b +} + +// WithResourceVersion sets the ResourceVersion field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the ResourceVersion field is set to the value of the last call. +func (b *WorkspaceAuthenticationConfigurationApplyConfiguration) WithResourceVersion(value string) *WorkspaceAuthenticationConfigurationApplyConfiguration { + b.ensureObjectMetaApplyConfigurationExists() + b.ObjectMetaApplyConfiguration.ResourceVersion = &value + return b +} + +// WithGeneration sets the Generation field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the Generation field is set to the value of the last call. +func (b *WorkspaceAuthenticationConfigurationApplyConfiguration) WithGeneration(value int64) *WorkspaceAuthenticationConfigurationApplyConfiguration { + b.ensureObjectMetaApplyConfigurationExists() + b.ObjectMetaApplyConfiguration.Generation = &value + return b +} + +// WithCreationTimestamp sets the CreationTimestamp field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the CreationTimestamp field is set to the value of the last call. +func (b *WorkspaceAuthenticationConfigurationApplyConfiguration) WithCreationTimestamp(value metav1.Time) *WorkspaceAuthenticationConfigurationApplyConfiguration { + b.ensureObjectMetaApplyConfigurationExists() + b.ObjectMetaApplyConfiguration.CreationTimestamp = &value + return b +} + +// WithDeletionTimestamp sets the DeletionTimestamp field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the DeletionTimestamp field is set to the value of the last call. +func (b *WorkspaceAuthenticationConfigurationApplyConfiguration) WithDeletionTimestamp(value metav1.Time) *WorkspaceAuthenticationConfigurationApplyConfiguration { + b.ensureObjectMetaApplyConfigurationExists() + b.ObjectMetaApplyConfiguration.DeletionTimestamp = &value + return b +} + +// WithDeletionGracePeriodSeconds sets the DeletionGracePeriodSeconds field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the DeletionGracePeriodSeconds field is set to the value of the last call. +func (b *WorkspaceAuthenticationConfigurationApplyConfiguration) WithDeletionGracePeriodSeconds(value int64) *WorkspaceAuthenticationConfigurationApplyConfiguration { + b.ensureObjectMetaApplyConfigurationExists() + b.ObjectMetaApplyConfiguration.DeletionGracePeriodSeconds = &value + return b +} + +// WithLabels puts the entries into the Labels field in the declarative configuration +// and returns the receiver, so that objects can be build by chaining "With" function invocations. +// If called multiple times, the entries provided by each call will be put on the Labels field, +// overwriting an existing map entries in Labels field with the same key. +func (b *WorkspaceAuthenticationConfigurationApplyConfiguration) WithLabels(entries map[string]string) *WorkspaceAuthenticationConfigurationApplyConfiguration { + b.ensureObjectMetaApplyConfigurationExists() + if b.ObjectMetaApplyConfiguration.Labels == nil && len(entries) > 0 { + b.ObjectMetaApplyConfiguration.Labels = make(map[string]string, len(entries)) + } + for k, v := range entries { + b.ObjectMetaApplyConfiguration.Labels[k] = v + } + return b +} + +// WithAnnotations puts the entries into the Annotations field in the declarative configuration +// and returns the receiver, so that objects can be build by chaining "With" function invocations. +// If called multiple times, the entries provided by each call will be put on the Annotations field, +// overwriting an existing map entries in Annotations field with the same key. +func (b *WorkspaceAuthenticationConfigurationApplyConfiguration) WithAnnotations(entries map[string]string) *WorkspaceAuthenticationConfigurationApplyConfiguration { + b.ensureObjectMetaApplyConfigurationExists() + if b.ObjectMetaApplyConfiguration.Annotations == nil && len(entries) > 0 { + b.ObjectMetaApplyConfiguration.Annotations = make(map[string]string, len(entries)) + } + for k, v := range entries { + b.ObjectMetaApplyConfiguration.Annotations[k] = v + } + return b +} + +// WithOwnerReferences adds the given value to the OwnerReferences field in the declarative configuration +// and returns the receiver, so that objects can be build by chaining "With" function invocations. +// If called multiple times, values provided by each call will be appended to the OwnerReferences field. +func (b *WorkspaceAuthenticationConfigurationApplyConfiguration) WithOwnerReferences(values ...*v1.OwnerReferenceApplyConfiguration) *WorkspaceAuthenticationConfigurationApplyConfiguration { + b.ensureObjectMetaApplyConfigurationExists() + for i := range values { + if values[i] == nil { + panic("nil value passed to WithOwnerReferences") + } + b.ObjectMetaApplyConfiguration.OwnerReferences = append(b.ObjectMetaApplyConfiguration.OwnerReferences, *values[i]) + } + return b +} + +// WithFinalizers adds the given value to the Finalizers field in the declarative configuration +// and returns the receiver, so that objects can be build by chaining "With" function invocations. +// If called multiple times, values provided by each call will be appended to the Finalizers field. +func (b *WorkspaceAuthenticationConfigurationApplyConfiguration) WithFinalizers(values ...string) *WorkspaceAuthenticationConfigurationApplyConfiguration { + b.ensureObjectMetaApplyConfigurationExists() + for i := range values { + b.ObjectMetaApplyConfiguration.Finalizers = append(b.ObjectMetaApplyConfiguration.Finalizers, values[i]) + } + return b +} + +func (b *WorkspaceAuthenticationConfigurationApplyConfiguration) ensureObjectMetaApplyConfigurationExists() { + if b.ObjectMetaApplyConfiguration == nil { + b.ObjectMetaApplyConfiguration = &v1.ObjectMetaApplyConfiguration{} + } +} + +// WithSpec sets the Spec field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the Spec field is set to the value of the last call. +func (b *WorkspaceAuthenticationConfigurationApplyConfiguration) WithSpec(value *WorkspaceAuthenticationConfigurationSpecApplyConfiguration) *WorkspaceAuthenticationConfigurationApplyConfiguration { + b.Spec = value + return b +} + +// GetName retrieves the value of the Name field in the declarative configuration. +func (b *WorkspaceAuthenticationConfigurationApplyConfiguration) GetName() *string { + b.ensureObjectMetaApplyConfigurationExists() + return b.ObjectMetaApplyConfiguration.Name +} diff --git a/sdk/client/applyconfiguration/tenancy/v1alpha1/workspaceauthenticationconfigurationspec.go b/sdk/client/applyconfiguration/tenancy/v1alpha1/workspaceauthenticationconfigurationspec.go new file mode 100644 index 00000000000..a03eb400808 --- /dev/null +++ b/sdk/client/applyconfiguration/tenancy/v1alpha1/workspaceauthenticationconfigurationspec.go @@ -0,0 +1,44 @@ +/* +Copyright The KCP Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Code generated by applyconfiguration-gen. DO NOT EDIT. + +package v1alpha1 + +// WorkspaceAuthenticationConfigurationSpecApplyConfiguration represents a declarative configuration of the WorkspaceAuthenticationConfigurationSpec type for use +// with apply. +type WorkspaceAuthenticationConfigurationSpecApplyConfiguration struct { + JWT []JWTAuthenticatorApplyConfiguration `json:"jwt,omitempty"` +} + +// WorkspaceAuthenticationConfigurationSpecApplyConfiguration constructs a declarative configuration of the WorkspaceAuthenticationConfigurationSpec type for use with +// apply. +func WorkspaceAuthenticationConfigurationSpec() *WorkspaceAuthenticationConfigurationSpecApplyConfiguration { + return &WorkspaceAuthenticationConfigurationSpecApplyConfiguration{} +} + +// WithJWT adds the given value to the JWT field in the declarative configuration +// and returns the receiver, so that objects can be build by chaining "With" function invocations. +// If called multiple times, values provided by each call will be appended to the JWT field. +func (b *WorkspaceAuthenticationConfigurationSpecApplyConfiguration) WithJWT(values ...*JWTAuthenticatorApplyConfiguration) *WorkspaceAuthenticationConfigurationSpecApplyConfiguration { + for i := range values { + if values[i] == nil { + panic("nil value passed to WithJWT") + } + b.JWT = append(b.JWT, *values[i]) + } + return b +} diff --git a/sdk/client/applyconfiguration/tenancy/v1alpha1/workspacetypespec.go b/sdk/client/applyconfiguration/tenancy/v1alpha1/workspacetypespec.go index 8f33b7368a8..46d112cc239 100644 --- a/sdk/client/applyconfiguration/tenancy/v1alpha1/workspacetypespec.go +++ b/sdk/client/applyconfiguration/tenancy/v1alpha1/workspacetypespec.go @@ -25,14 +25,15 @@ import ( // WorkspaceTypeSpecApplyConfiguration represents a declarative configuration of the WorkspaceTypeSpec type for use // with apply. type WorkspaceTypeSpecApplyConfiguration struct { - Initializer *bool `json:"initializer,omitempty"` - Extend *WorkspaceTypeExtensionApplyConfiguration `json:"extend,omitempty"` - AdditionalWorkspaceLabels map[string]string `json:"additionalWorkspaceLabels,omitempty"` - DefaultChildWorkspaceType *WorkspaceTypeReferenceApplyConfiguration `json:"defaultChildWorkspaceType,omitempty"` - LimitAllowedChildren *WorkspaceTypeSelectorApplyConfiguration `json:"limitAllowedChildren,omitempty"` - LimitAllowedParents *WorkspaceTypeSelectorApplyConfiguration `json:"limitAllowedParents,omitempty"` - DefaultAPIBindings []APIExportReferenceApplyConfiguration `json:"defaultAPIBindings,omitempty"` - DefaultAPIBindingLifecycle *tenancyv1alpha1.APIBindingLifecycleMode `json:"defaultAPIBindingLifecycle,omitempty"` + Initializer *bool `json:"initializer,omitempty"` + Extend *WorkspaceTypeExtensionApplyConfiguration `json:"extend,omitempty"` + AdditionalWorkspaceLabels map[string]string `json:"additionalWorkspaceLabels,omitempty"` + DefaultChildWorkspaceType *WorkspaceTypeReferenceApplyConfiguration `json:"defaultChildWorkspaceType,omitempty"` + LimitAllowedChildren *WorkspaceTypeSelectorApplyConfiguration `json:"limitAllowedChildren,omitempty"` + LimitAllowedParents *WorkspaceTypeSelectorApplyConfiguration `json:"limitAllowedParents,omitempty"` + DefaultAPIBindings []APIExportReferenceApplyConfiguration `json:"defaultAPIBindings,omitempty"` + DefaultAPIBindingLifecycle *tenancyv1alpha1.APIBindingLifecycleMode `json:"defaultAPIBindingLifecycle,omitempty"` + AuthenticationConfigurations []AuthenticationConfigurationReferenceApplyConfiguration `json:"authenticationConfigurations,omitempty"` } // WorkspaceTypeSpecApplyConfiguration constructs a declarative configuration of the WorkspaceTypeSpec type for use with @@ -115,3 +116,16 @@ func (b *WorkspaceTypeSpecApplyConfiguration) WithDefaultAPIBindingLifecycle(val b.DefaultAPIBindingLifecycle = &value return b } + +// WithAuthenticationConfigurations adds the given value to the AuthenticationConfigurations field in the declarative configuration +// and returns the receiver, so that objects can be build by chaining "With" function invocations. +// If called multiple times, values provided by each call will be appended to the AuthenticationConfigurations field. +func (b *WorkspaceTypeSpecApplyConfiguration) WithAuthenticationConfigurations(values ...*AuthenticationConfigurationReferenceApplyConfiguration) *WorkspaceTypeSpecApplyConfiguration { + for i := range values { + if values[i] == nil { + panic("nil value passed to WithAuthenticationConfigurations") + } + b.AuthenticationConfigurations = append(b.AuthenticationConfigurations, *values[i]) + } + return b +} diff --git a/sdk/client/applyconfiguration/utils.go b/sdk/client/applyconfiguration/utils.go index e7aebd33683..896e88df411 100644 --- a/sdk/client/applyconfiguration/utils.go +++ b/sdk/client/applyconfiguration/utils.go @@ -222,14 +222,36 @@ func ForKind(kind schema.GroupVersionKind) interface{} { // Group=tenancy.kcp.io, Version=v1alpha1 case tenancyv1alpha1.SchemeGroupVersion.WithKind("APIExportReference"): return &applyconfigurationtenancyv1alpha1.APIExportReferenceApplyConfiguration{} + case tenancyv1alpha1.SchemeGroupVersion.WithKind("AuthenticationConfigurationReference"): + return &applyconfigurationtenancyv1alpha1.AuthenticationConfigurationReferenceApplyConfiguration{} + case tenancyv1alpha1.SchemeGroupVersion.WithKind("ClaimMappings"): + return &applyconfigurationtenancyv1alpha1.ClaimMappingsApplyConfiguration{} + case tenancyv1alpha1.SchemeGroupVersion.WithKind("ClaimOrExpression"): + return &applyconfigurationtenancyv1alpha1.ClaimOrExpressionApplyConfiguration{} + case tenancyv1alpha1.SchemeGroupVersion.WithKind("ClaimValidationRule"): + return &applyconfigurationtenancyv1alpha1.ClaimValidationRuleApplyConfiguration{} + case tenancyv1alpha1.SchemeGroupVersion.WithKind("ExtraMapping"): + return &applyconfigurationtenancyv1alpha1.ExtraMappingApplyConfiguration{} + case tenancyv1alpha1.SchemeGroupVersion.WithKind("Issuer"): + return &applyconfigurationtenancyv1alpha1.IssuerApplyConfiguration{} + case tenancyv1alpha1.SchemeGroupVersion.WithKind("JWTAuthenticator"): + return &applyconfigurationtenancyv1alpha1.JWTAuthenticatorApplyConfiguration{} case tenancyv1alpha1.SchemeGroupVersion.WithKind("Mount"): return &applyconfigurationtenancyv1alpha1.MountApplyConfiguration{} case tenancyv1alpha1.SchemeGroupVersion.WithKind("ObjectReference"): return &applyconfigurationtenancyv1alpha1.ObjectReferenceApplyConfiguration{} + case tenancyv1alpha1.SchemeGroupVersion.WithKind("PrefixedClaimOrExpression"): + return &applyconfigurationtenancyv1alpha1.PrefixedClaimOrExpressionApplyConfiguration{} + case tenancyv1alpha1.SchemeGroupVersion.WithKind("UserValidationRule"): + return &applyconfigurationtenancyv1alpha1.UserValidationRuleApplyConfiguration{} case tenancyv1alpha1.SchemeGroupVersion.WithKind("VirtualWorkspace"): return &applyconfigurationtenancyv1alpha1.VirtualWorkspaceApplyConfiguration{} case tenancyv1alpha1.SchemeGroupVersion.WithKind("Workspace"): return &applyconfigurationtenancyv1alpha1.WorkspaceApplyConfiguration{} + case tenancyv1alpha1.SchemeGroupVersion.WithKind("WorkspaceAuthenticationConfiguration"): + return &applyconfigurationtenancyv1alpha1.WorkspaceAuthenticationConfigurationApplyConfiguration{} + case tenancyv1alpha1.SchemeGroupVersion.WithKind("WorkspaceAuthenticationConfigurationSpec"): + return &applyconfigurationtenancyv1alpha1.WorkspaceAuthenticationConfigurationSpecApplyConfiguration{} case tenancyv1alpha1.SchemeGroupVersion.WithKind("WorkspaceLocation"): return &applyconfigurationtenancyv1alpha1.WorkspaceLocationApplyConfiguration{} case tenancyv1alpha1.SchemeGroupVersion.WithKind("WorkspaceSpec"): diff --git a/sdk/client/clientset/versioned/cluster/typed/tenancy/v1alpha1/fake/tenancy_client.go b/sdk/client/clientset/versioned/cluster/typed/tenancy/v1alpha1/fake/tenancy_client.go index 56caf2716d7..005afed38c5 100644 --- a/sdk/client/clientset/versioned/cluster/typed/tenancy/v1alpha1/fake/tenancy_client.go +++ b/sdk/client/clientset/versioned/cluster/typed/tenancy/v1alpha1/fake/tenancy_client.go @@ -45,6 +45,10 @@ func (c *TenancyV1alpha1ClusterClient) Workspaces() kcptenancyv1alpha1.Workspace return newFakeWorkspaceClusterClient(c) } +func (c *TenancyV1alpha1ClusterClient) WorkspaceAuthenticationConfigurations() kcptenancyv1alpha1.WorkspaceAuthenticationConfigurationClusterInterface { + return newFakeWorkspaceAuthenticationConfigurationClusterClient(c) +} + func (c *TenancyV1alpha1ClusterClient) WorkspaceTypes() kcptenancyv1alpha1.WorkspaceTypeClusterInterface { return newFakeWorkspaceTypeClusterClient(c) } @@ -58,6 +62,10 @@ func (c *TenancyV1alpha1Client) Workspaces() tenancyv1alpha1.WorkspaceInterface return newFakeWorkspaceClient(c.Fake, c.ClusterPath) } +func (c *TenancyV1alpha1Client) WorkspaceAuthenticationConfigurations() tenancyv1alpha1.WorkspaceAuthenticationConfigurationInterface { + return newFakeWorkspaceAuthenticationConfigurationClient(c.Fake, c.ClusterPath) +} + func (c *TenancyV1alpha1Client) WorkspaceTypes() tenancyv1alpha1.WorkspaceTypeInterface { return newFakeWorkspaceTypeClient(c.Fake, c.ClusterPath) } diff --git a/sdk/client/clientset/versioned/cluster/typed/tenancy/v1alpha1/fake/workspaceauthenticationconfiguration.go b/sdk/client/clientset/versioned/cluster/typed/tenancy/v1alpha1/fake/workspaceauthenticationconfiguration.go new file mode 100644 index 00000000000..88e03a3cddc --- /dev/null +++ b/sdk/client/clientset/versioned/cluster/typed/tenancy/v1alpha1/fake/workspaceauthenticationconfiguration.go @@ -0,0 +1,98 @@ +/* +Copyright The KCP Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Code generated by cluster-client-gen. DO NOT EDIT. + +package fake + +import ( + kcpgentype "github.com/kcp-dev/client-go/third_party/k8s.io/client-go/gentype" + kcptesting "github.com/kcp-dev/client-go/third_party/k8s.io/client-go/testing" + "github.com/kcp-dev/logicalcluster/v3" + + tenancyv1alpha1 "github.com/kcp-dev/kcp/sdk/apis/tenancy/v1alpha1" + kcpv1alpha1 "github.com/kcp-dev/kcp/sdk/client/applyconfiguration/tenancy/v1alpha1" + typedkcptenancyv1alpha1 "github.com/kcp-dev/kcp/sdk/client/clientset/versioned/cluster/typed/tenancy/v1alpha1" + typedtenancyv1alpha1 "github.com/kcp-dev/kcp/sdk/client/clientset/versioned/typed/tenancy/v1alpha1" +) + +// workspaceAuthenticationConfigurationClusterClient implements WorkspaceAuthenticationConfigurationClusterInterface +type workspaceAuthenticationConfigurationClusterClient struct { + *kcpgentype.FakeClusterClientWithList[*tenancyv1alpha1.WorkspaceAuthenticationConfiguration, *tenancyv1alpha1.WorkspaceAuthenticationConfigurationList] + Fake *kcptesting.Fake +} + +func newFakeWorkspaceAuthenticationConfigurationClusterClient(fake *TenancyV1alpha1ClusterClient) typedkcptenancyv1alpha1.WorkspaceAuthenticationConfigurationClusterInterface { + return &workspaceAuthenticationConfigurationClusterClient{ + kcpgentype.NewFakeClusterClientWithList[*tenancyv1alpha1.WorkspaceAuthenticationConfiguration, *tenancyv1alpha1.WorkspaceAuthenticationConfigurationList]( + fake.Fake, + tenancyv1alpha1.SchemeGroupVersion.WithResource("workspaceauthenticationconfigurations"), + tenancyv1alpha1.SchemeGroupVersion.WithKind("WorkspaceAuthenticationConfiguration"), + func() *tenancyv1alpha1.WorkspaceAuthenticationConfiguration { + return &tenancyv1alpha1.WorkspaceAuthenticationConfiguration{} + }, + func() *tenancyv1alpha1.WorkspaceAuthenticationConfigurationList { + return &tenancyv1alpha1.WorkspaceAuthenticationConfigurationList{} + }, + func(dst, src *tenancyv1alpha1.WorkspaceAuthenticationConfigurationList) { dst.ListMeta = src.ListMeta }, + func(list *tenancyv1alpha1.WorkspaceAuthenticationConfigurationList) []*tenancyv1alpha1.WorkspaceAuthenticationConfiguration { + return kcpgentype.ToPointerSlice(list.Items) + }, + func(list *tenancyv1alpha1.WorkspaceAuthenticationConfigurationList, items []*tenancyv1alpha1.WorkspaceAuthenticationConfiguration) { + list.Items = kcpgentype.FromPointerSlice(items) + }, + ), + fake.Fake, + } +} + +func (c *workspaceAuthenticationConfigurationClusterClient) Cluster(cluster logicalcluster.Path) typedtenancyv1alpha1.WorkspaceAuthenticationConfigurationInterface { + return newFakeWorkspaceAuthenticationConfigurationClient(c.Fake, cluster) +} + +// workspaceAuthenticationConfigurationScopedClient implements WorkspaceAuthenticationConfigurationInterface +type workspaceAuthenticationConfigurationScopedClient struct { + *kcpgentype.FakeClientWithListAndApply[*tenancyv1alpha1.WorkspaceAuthenticationConfiguration, *tenancyv1alpha1.WorkspaceAuthenticationConfigurationList, *kcpv1alpha1.WorkspaceAuthenticationConfigurationApplyConfiguration] + Fake *kcptesting.Fake + ClusterPath logicalcluster.Path +} + +func newFakeWorkspaceAuthenticationConfigurationClient(fake *kcptesting.Fake, clusterPath logicalcluster.Path) typedtenancyv1alpha1.WorkspaceAuthenticationConfigurationInterface { + return &workspaceAuthenticationConfigurationScopedClient{ + kcpgentype.NewFakeClientWithListAndApply[*tenancyv1alpha1.WorkspaceAuthenticationConfiguration, *tenancyv1alpha1.WorkspaceAuthenticationConfigurationList, *kcpv1alpha1.WorkspaceAuthenticationConfigurationApplyConfiguration]( + fake, + clusterPath, + "", + tenancyv1alpha1.SchemeGroupVersion.WithResource("workspaceauthenticationconfigurations"), + tenancyv1alpha1.SchemeGroupVersion.WithKind("WorkspaceAuthenticationConfiguration"), + func() *tenancyv1alpha1.WorkspaceAuthenticationConfiguration { + return &tenancyv1alpha1.WorkspaceAuthenticationConfiguration{} + }, + func() *tenancyv1alpha1.WorkspaceAuthenticationConfigurationList { + return &tenancyv1alpha1.WorkspaceAuthenticationConfigurationList{} + }, + func(dst, src *tenancyv1alpha1.WorkspaceAuthenticationConfigurationList) { dst.ListMeta = src.ListMeta }, + func(list *tenancyv1alpha1.WorkspaceAuthenticationConfigurationList) []*tenancyv1alpha1.WorkspaceAuthenticationConfiguration { + return kcpgentype.ToPointerSlice(list.Items) + }, + func(list *tenancyv1alpha1.WorkspaceAuthenticationConfigurationList, items []*tenancyv1alpha1.WorkspaceAuthenticationConfiguration) { + list.Items = kcpgentype.FromPointerSlice(items) + }, + ), + fake, + clusterPath, + } +} diff --git a/sdk/client/clientset/versioned/cluster/typed/tenancy/v1alpha1/generated_expansion.go b/sdk/client/clientset/versioned/cluster/typed/tenancy/v1alpha1/generated_expansion.go index 89f2105128c..85633c6ca39 100644 --- a/sdk/client/clientset/versioned/cluster/typed/tenancy/v1alpha1/generated_expansion.go +++ b/sdk/client/clientset/versioned/cluster/typed/tenancy/v1alpha1/generated_expansion.go @@ -20,4 +20,6 @@ package v1alpha1 type WorkspaceClusterExpansion interface{} +type WorkspaceAuthenticationConfigurationClusterExpansion interface{} + type WorkspaceTypeClusterExpansion interface{} diff --git a/sdk/client/clientset/versioned/cluster/typed/tenancy/v1alpha1/tenancy_client.go b/sdk/client/clientset/versioned/cluster/typed/tenancy/v1alpha1/tenancy_client.go index 9c882ade025..1bafedc978a 100644 --- a/sdk/client/clientset/versioned/cluster/typed/tenancy/v1alpha1/tenancy_client.go +++ b/sdk/client/clientset/versioned/cluster/typed/tenancy/v1alpha1/tenancy_client.go @@ -34,6 +34,7 @@ import ( type TenancyV1alpha1ClusterInterface interface { TenancyV1alpha1ClusterScoper WorkspacesClusterGetter + WorkspaceAuthenticationConfigurationsClusterGetter WorkspaceTypesClusterGetter } @@ -57,6 +58,10 @@ func (c *TenancyV1alpha1ClusterClient) Workspaces() WorkspaceClusterInterface { return &workspacesClusterInterface{clientCache: c.clientCache} } +func (c *TenancyV1alpha1ClusterClient) WorkspaceAuthenticationConfigurations() WorkspaceAuthenticationConfigurationClusterInterface { + return &workspaceAuthenticationConfigurationsClusterInterface{clientCache: c.clientCache} +} + func (c *TenancyV1alpha1ClusterClient) WorkspaceTypes() WorkspaceTypeClusterInterface { return &workspaceTypesClusterInterface{clientCache: c.clientCache} } diff --git a/sdk/client/clientset/versioned/cluster/typed/tenancy/v1alpha1/workspaceauthenticationconfiguration.go b/sdk/client/clientset/versioned/cluster/typed/tenancy/v1alpha1/workspaceauthenticationconfiguration.go new file mode 100644 index 00000000000..6f9bb0cd912 --- /dev/null +++ b/sdk/client/clientset/versioned/cluster/typed/tenancy/v1alpha1/workspaceauthenticationconfiguration.go @@ -0,0 +1,70 @@ +/* +Copyright The KCP Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Code generated by cluster-client-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + context "context" + + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + watch "k8s.io/apimachinery/pkg/watch" + + kcpclient "github.com/kcp-dev/apimachinery/v2/pkg/client" + "github.com/kcp-dev/logicalcluster/v3" + + kcptenancyv1alpha1 "github.com/kcp-dev/kcp/sdk/apis/tenancy/v1alpha1" + kcpv1alpha1 "github.com/kcp-dev/kcp/sdk/client/clientset/versioned/typed/tenancy/v1alpha1" +) + +// WorkspaceAuthenticationConfigurationsClusterGetter has a method to return a WorkspaceAuthenticationConfigurationClusterInterface. +// A group's cluster client should implement this interface. +type WorkspaceAuthenticationConfigurationsClusterGetter interface { + WorkspaceAuthenticationConfigurations() WorkspaceAuthenticationConfigurationClusterInterface +} + +// WorkspaceAuthenticationConfigurationClusterInterface can operate on WorkspaceAuthenticationConfigurations across all clusters, +// or scope down to one cluster and return a kcpv1alpha1.WorkspaceAuthenticationConfigurationInterface. +type WorkspaceAuthenticationConfigurationClusterInterface interface { + Cluster(logicalcluster.Path) kcpv1alpha1.WorkspaceAuthenticationConfigurationInterface + List(ctx context.Context, opts v1.ListOptions) (*kcptenancyv1alpha1.WorkspaceAuthenticationConfigurationList, error) + Watch(ctx context.Context, opts v1.ListOptions) (watch.Interface, error) + WorkspaceAuthenticationConfigurationClusterExpansion +} + +type workspaceAuthenticationConfigurationsClusterInterface struct { + clientCache kcpclient.Cache[*kcpv1alpha1.TenancyV1alpha1Client] +} + +// Cluster scopes the client down to a particular cluster. +func (c *workspaceAuthenticationConfigurationsClusterInterface) Cluster(clusterPath logicalcluster.Path) kcpv1alpha1.WorkspaceAuthenticationConfigurationInterface { + if clusterPath == logicalcluster.Wildcard { + panic("A specific cluster must be provided when scoping, not the wildcard.") + } + + return c.clientCache.ClusterOrDie(clusterPath).WorkspaceAuthenticationConfigurations() +} + +// List returns the entire collection of all WorkspaceAuthenticationConfigurations across all clusters. +func (c *workspaceAuthenticationConfigurationsClusterInterface) List(ctx context.Context, opts v1.ListOptions) (*kcptenancyv1alpha1.WorkspaceAuthenticationConfigurationList, error) { + return c.clientCache.ClusterOrDie(logicalcluster.Wildcard).WorkspaceAuthenticationConfigurations().List(ctx, opts) +} + +// Watch begins to watch all WorkspaceAuthenticationConfigurations across all clusters. +func (c *workspaceAuthenticationConfigurationsClusterInterface) Watch(ctx context.Context, opts v1.ListOptions) (watch.Interface, error) { + return c.clientCache.ClusterOrDie(logicalcluster.Wildcard).WorkspaceAuthenticationConfigurations().Watch(ctx, opts) +} diff --git a/sdk/client/clientset/versioned/typed/tenancy/v1alpha1/fake/fake_tenancy_client.go b/sdk/client/clientset/versioned/typed/tenancy/v1alpha1/fake/fake_tenancy_client.go index c67fc94fe86..dc182342606 100644 --- a/sdk/client/clientset/versioned/typed/tenancy/v1alpha1/fake/fake_tenancy_client.go +++ b/sdk/client/clientset/versioned/typed/tenancy/v1alpha1/fake/fake_tenancy_client.go @@ -33,6 +33,10 @@ func (c *FakeTenancyV1alpha1) Workspaces() v1alpha1.WorkspaceInterface { return newFakeWorkspaces(c) } +func (c *FakeTenancyV1alpha1) WorkspaceAuthenticationConfigurations() v1alpha1.WorkspaceAuthenticationConfigurationInterface { + return newFakeWorkspaceAuthenticationConfigurations(c) +} + func (c *FakeTenancyV1alpha1) WorkspaceTypes() v1alpha1.WorkspaceTypeInterface { return newFakeWorkspaceTypes(c) } diff --git a/sdk/client/clientset/versioned/typed/tenancy/v1alpha1/fake/fake_workspaceauthenticationconfiguration.go b/sdk/client/clientset/versioned/typed/tenancy/v1alpha1/fake/fake_workspaceauthenticationconfiguration.go new file mode 100644 index 00000000000..ef12021b006 --- /dev/null +++ b/sdk/client/clientset/versioned/typed/tenancy/v1alpha1/fake/fake_workspaceauthenticationconfiguration.go @@ -0,0 +1,58 @@ +/* +Copyright The KCP Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Code generated by client-gen. DO NOT EDIT. + +package fake + +import ( + gentype "k8s.io/client-go/gentype" + + v1alpha1 "github.com/kcp-dev/kcp/sdk/apis/tenancy/v1alpha1" + tenancyv1alpha1 "github.com/kcp-dev/kcp/sdk/client/applyconfiguration/tenancy/v1alpha1" + typedtenancyv1alpha1 "github.com/kcp-dev/kcp/sdk/client/clientset/versioned/typed/tenancy/v1alpha1" +) + +// fakeWorkspaceAuthenticationConfigurations implements WorkspaceAuthenticationConfigurationInterface +type fakeWorkspaceAuthenticationConfigurations struct { + *gentype.FakeClientWithListAndApply[*v1alpha1.WorkspaceAuthenticationConfiguration, *v1alpha1.WorkspaceAuthenticationConfigurationList, *tenancyv1alpha1.WorkspaceAuthenticationConfigurationApplyConfiguration] + Fake *FakeTenancyV1alpha1 +} + +func newFakeWorkspaceAuthenticationConfigurations(fake *FakeTenancyV1alpha1) typedtenancyv1alpha1.WorkspaceAuthenticationConfigurationInterface { + return &fakeWorkspaceAuthenticationConfigurations{ + gentype.NewFakeClientWithListAndApply[*v1alpha1.WorkspaceAuthenticationConfiguration, *v1alpha1.WorkspaceAuthenticationConfigurationList, *tenancyv1alpha1.WorkspaceAuthenticationConfigurationApplyConfiguration]( + fake.Fake, + "", + v1alpha1.SchemeGroupVersion.WithResource("workspaceauthenticationconfigurations"), + v1alpha1.SchemeGroupVersion.WithKind("WorkspaceAuthenticationConfiguration"), + func() *v1alpha1.WorkspaceAuthenticationConfiguration { + return &v1alpha1.WorkspaceAuthenticationConfiguration{} + }, + func() *v1alpha1.WorkspaceAuthenticationConfigurationList { + return &v1alpha1.WorkspaceAuthenticationConfigurationList{} + }, + func(dst, src *v1alpha1.WorkspaceAuthenticationConfigurationList) { dst.ListMeta = src.ListMeta }, + func(list *v1alpha1.WorkspaceAuthenticationConfigurationList) []*v1alpha1.WorkspaceAuthenticationConfiguration { + return gentype.ToPointerSlice(list.Items) + }, + func(list *v1alpha1.WorkspaceAuthenticationConfigurationList, items []*v1alpha1.WorkspaceAuthenticationConfiguration) { + list.Items = gentype.FromPointerSlice(items) + }, + ), + fake, + } +} diff --git a/sdk/client/clientset/versioned/typed/tenancy/v1alpha1/generated_expansion.go b/sdk/client/clientset/versioned/typed/tenancy/v1alpha1/generated_expansion.go index 7a7831b81b7..330d10d9884 100644 --- a/sdk/client/clientset/versioned/typed/tenancy/v1alpha1/generated_expansion.go +++ b/sdk/client/clientset/versioned/typed/tenancy/v1alpha1/generated_expansion.go @@ -20,4 +20,6 @@ package v1alpha1 type WorkspaceExpansion interface{} +type WorkspaceAuthenticationConfigurationExpansion interface{} + type WorkspaceTypeExpansion interface{} diff --git a/sdk/client/clientset/versioned/typed/tenancy/v1alpha1/tenancy_client.go b/sdk/client/clientset/versioned/typed/tenancy/v1alpha1/tenancy_client.go index b25c7bd3837..289995c527e 100644 --- a/sdk/client/clientset/versioned/typed/tenancy/v1alpha1/tenancy_client.go +++ b/sdk/client/clientset/versioned/typed/tenancy/v1alpha1/tenancy_client.go @@ -30,6 +30,7 @@ import ( type TenancyV1alpha1Interface interface { RESTClient() rest.Interface WorkspacesGetter + WorkspaceAuthenticationConfigurationsGetter WorkspaceTypesGetter } @@ -42,6 +43,10 @@ func (c *TenancyV1alpha1Client) Workspaces() WorkspaceInterface { return newWorkspaces(c) } +func (c *TenancyV1alpha1Client) WorkspaceAuthenticationConfigurations() WorkspaceAuthenticationConfigurationInterface { + return newWorkspaceAuthenticationConfigurations(c) +} + func (c *TenancyV1alpha1Client) WorkspaceTypes() WorkspaceTypeInterface { return newWorkspaceTypes(c) } diff --git a/sdk/client/clientset/versioned/typed/tenancy/v1alpha1/workspaceauthenticationconfiguration.go b/sdk/client/clientset/versioned/typed/tenancy/v1alpha1/workspaceauthenticationconfiguration.go new file mode 100644 index 00000000000..2d4ac8c601c --- /dev/null +++ b/sdk/client/clientset/versioned/typed/tenancy/v1alpha1/workspaceauthenticationconfiguration.go @@ -0,0 +1,75 @@ +/* +Copyright The KCP Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Code generated by client-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + context "context" + + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + types "k8s.io/apimachinery/pkg/types" + watch "k8s.io/apimachinery/pkg/watch" + gentype "k8s.io/client-go/gentype" + + tenancyv1alpha1 "github.com/kcp-dev/kcp/sdk/apis/tenancy/v1alpha1" + applyconfigurationtenancyv1alpha1 "github.com/kcp-dev/kcp/sdk/client/applyconfiguration/tenancy/v1alpha1" + scheme "github.com/kcp-dev/kcp/sdk/client/clientset/versioned/scheme" +) + +// WorkspaceAuthenticationConfigurationsGetter has a method to return a WorkspaceAuthenticationConfigurationInterface. +// A group's client should implement this interface. +type WorkspaceAuthenticationConfigurationsGetter interface { + WorkspaceAuthenticationConfigurations() WorkspaceAuthenticationConfigurationInterface +} + +// WorkspaceAuthenticationConfigurationInterface has methods to work with WorkspaceAuthenticationConfiguration resources. +type WorkspaceAuthenticationConfigurationInterface interface { + Create(ctx context.Context, workspaceAuthenticationConfiguration *tenancyv1alpha1.WorkspaceAuthenticationConfiguration, opts v1.CreateOptions) (*tenancyv1alpha1.WorkspaceAuthenticationConfiguration, error) + Update(ctx context.Context, workspaceAuthenticationConfiguration *tenancyv1alpha1.WorkspaceAuthenticationConfiguration, opts v1.UpdateOptions) (*tenancyv1alpha1.WorkspaceAuthenticationConfiguration, error) + Delete(ctx context.Context, name string, opts v1.DeleteOptions) error + DeleteCollection(ctx context.Context, opts v1.DeleteOptions, listOpts v1.ListOptions) error + Get(ctx context.Context, name string, opts v1.GetOptions) (*tenancyv1alpha1.WorkspaceAuthenticationConfiguration, error) + List(ctx context.Context, opts v1.ListOptions) (*tenancyv1alpha1.WorkspaceAuthenticationConfigurationList, error) + Watch(ctx context.Context, opts v1.ListOptions) (watch.Interface, error) + Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts v1.PatchOptions, subresources ...string) (result *tenancyv1alpha1.WorkspaceAuthenticationConfiguration, err error) + Apply(ctx context.Context, workspaceAuthenticationConfiguration *applyconfigurationtenancyv1alpha1.WorkspaceAuthenticationConfigurationApplyConfiguration, opts v1.ApplyOptions) (result *tenancyv1alpha1.WorkspaceAuthenticationConfiguration, err error) + WorkspaceAuthenticationConfigurationExpansion +} + +// workspaceAuthenticationConfigurations implements WorkspaceAuthenticationConfigurationInterface +type workspaceAuthenticationConfigurations struct { + *gentype.ClientWithListAndApply[*tenancyv1alpha1.WorkspaceAuthenticationConfiguration, *tenancyv1alpha1.WorkspaceAuthenticationConfigurationList, *applyconfigurationtenancyv1alpha1.WorkspaceAuthenticationConfigurationApplyConfiguration] +} + +// newWorkspaceAuthenticationConfigurations returns a WorkspaceAuthenticationConfigurations +func newWorkspaceAuthenticationConfigurations(c *TenancyV1alpha1Client) *workspaceAuthenticationConfigurations { + return &workspaceAuthenticationConfigurations{ + gentype.NewClientWithListAndApply[*tenancyv1alpha1.WorkspaceAuthenticationConfiguration, *tenancyv1alpha1.WorkspaceAuthenticationConfigurationList, *applyconfigurationtenancyv1alpha1.WorkspaceAuthenticationConfigurationApplyConfiguration]( + "workspaceauthenticationconfigurations", + c.RESTClient(), + scheme.ParameterCodec, + "", + func() *tenancyv1alpha1.WorkspaceAuthenticationConfiguration { + return &tenancyv1alpha1.WorkspaceAuthenticationConfiguration{} + }, + func() *tenancyv1alpha1.WorkspaceAuthenticationConfigurationList { + return &tenancyv1alpha1.WorkspaceAuthenticationConfigurationList{} + }, + ), + } +} diff --git a/sdk/client/informers/externalversions/generic.go b/sdk/client/informers/externalversions/generic.go index 0506774e19b..22c98e6fdfb 100644 --- a/sdk/client/informers/externalversions/generic.go +++ b/sdk/client/informers/externalversions/generic.go @@ -134,6 +134,8 @@ func (f *sharedInformerFactory) ForResource(resource schema.GroupVersionResource // Group=tenancy.kcp.io, Version=v1alpha1 case kcptenancyv1alpha1.SchemeGroupVersion.WithResource("workspaces"): return &genericClusterInformer{resource: resource.GroupResource(), informer: f.Tenancy().V1alpha1().Workspaces().Informer()}, nil + case kcptenancyv1alpha1.SchemeGroupVersion.WithResource("workspaceauthenticationconfigurations"): + return &genericClusterInformer{resource: resource.GroupResource(), informer: f.Tenancy().V1alpha1().WorkspaceAuthenticationConfigurations().Informer()}, nil case kcptenancyv1alpha1.SchemeGroupVersion.WithResource("workspacetypes"): return &genericClusterInformer{resource: resource.GroupResource(), informer: f.Tenancy().V1alpha1().WorkspaceTypes().Informer()}, nil @@ -200,6 +202,9 @@ func (f *sharedScopedInformerFactory) ForResource(resource schema.GroupVersionRe case kcptenancyv1alpha1.SchemeGroupVersion.WithResource("workspaces"): informer := f.Tenancy().V1alpha1().Workspaces().Informer() return &genericInformer{lister: cache.NewGenericLister(informer.GetIndexer(), resource.GroupResource()), informer: informer}, nil + case kcptenancyv1alpha1.SchemeGroupVersion.WithResource("workspaceauthenticationconfigurations"): + informer := f.Tenancy().V1alpha1().WorkspaceAuthenticationConfigurations().Informer() + return &genericInformer{lister: cache.NewGenericLister(informer.GetIndexer(), resource.GroupResource()), informer: informer}, nil case kcptenancyv1alpha1.SchemeGroupVersion.WithResource("workspacetypes"): informer := f.Tenancy().V1alpha1().WorkspaceTypes().Informer() return &genericInformer{lister: cache.NewGenericLister(informer.GetIndexer(), resource.GroupResource()), informer: informer}, nil diff --git a/sdk/client/informers/externalversions/tenancy/v1alpha1/interface.go b/sdk/client/informers/externalversions/tenancy/v1alpha1/interface.go index b2c17893735..5876008837f 100644 --- a/sdk/client/informers/externalversions/tenancy/v1alpha1/interface.go +++ b/sdk/client/informers/externalversions/tenancy/v1alpha1/interface.go @@ -25,6 +25,8 @@ import ( type ClusterInterface interface { // Workspaces returns a WorkspaceClusterInformer. Workspaces() WorkspaceClusterInformer + // WorkspaceAuthenticationConfigurations returns a WorkspaceAuthenticationConfigurationClusterInformer. + WorkspaceAuthenticationConfigurations() WorkspaceAuthenticationConfigurationClusterInformer // WorkspaceTypes returns a WorkspaceTypeClusterInformer. WorkspaceTypes() WorkspaceTypeClusterInformer } @@ -44,6 +46,11 @@ func (v *version) Workspaces() WorkspaceClusterInformer { return &workspaceClusterInformer{factory: v.factory, tweakListOptions: v.tweakListOptions} } +// WorkspaceAuthenticationConfigurations returns a WorkspaceAuthenticationConfigurationClusterInformer. +func (v *version) WorkspaceAuthenticationConfigurations() WorkspaceAuthenticationConfigurationClusterInformer { + return &workspaceAuthenticationConfigurationClusterInformer{factory: v.factory, tweakListOptions: v.tweakListOptions} +} + // WorkspaceTypes returns a WorkspaceTypeClusterInformer. func (v *version) WorkspaceTypes() WorkspaceTypeClusterInformer { return &workspaceTypeClusterInformer{factory: v.factory, tweakListOptions: v.tweakListOptions} @@ -52,6 +59,8 @@ func (v *version) WorkspaceTypes() WorkspaceTypeClusterInformer { type Interface interface { // Workspaces returns a WorkspaceInformer. Workspaces() WorkspaceInformer + // WorkspaceAuthenticationConfigurations returns a WorkspaceAuthenticationConfigurationInformer. + WorkspaceAuthenticationConfigurations() WorkspaceAuthenticationConfigurationInformer // WorkspaceTypes returns a WorkspaceTypeInformer. WorkspaceTypes() WorkspaceTypeInformer } @@ -72,6 +81,11 @@ func (v *scopedVersion) Workspaces() WorkspaceInformer { return &workspaceScopedInformer{factory: v.factory, tweakListOptions: v.tweakListOptions} } +// WorkspaceAuthenticationConfigurations returns a WorkspaceAuthenticationConfigurationInformer. +func (v *scopedVersion) WorkspaceAuthenticationConfigurations() WorkspaceAuthenticationConfigurationInformer { + return &workspaceAuthenticationConfigurationScopedInformer{factory: v.factory, tweakListOptions: v.tweakListOptions} +} + // WorkspaceTypes returns a WorkspaceTypeInformer. func (v *scopedVersion) WorkspaceTypes() WorkspaceTypeInformer { return &workspaceTypeScopedInformer{factory: v.factory, tweakListOptions: v.tweakListOptions} diff --git a/sdk/client/informers/externalversions/tenancy/v1alpha1/workspaceauthenticationconfiguration.go b/sdk/client/informers/externalversions/tenancy/v1alpha1/workspaceauthenticationconfiguration.go new file mode 100644 index 00000000000..bde16aaffff --- /dev/null +++ b/sdk/client/informers/externalversions/tenancy/v1alpha1/workspaceauthenticationconfiguration.go @@ -0,0 +1,183 @@ +/* +Copyright The KCP Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Code generated by cluster-informer-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + context "context" + time "time" + + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + runtime "k8s.io/apimachinery/pkg/runtime" + watch "k8s.io/apimachinery/pkg/watch" + cache "k8s.io/client-go/tools/cache" + + kcpcache "github.com/kcp-dev/apimachinery/v2/pkg/cache" + kcpinformers "github.com/kcp-dev/apimachinery/v2/third_party/informers" + logicalcluster "github.com/kcp-dev/logicalcluster/v3" + + kcptenancyv1alpha1 "github.com/kcp-dev/kcp/sdk/apis/tenancy/v1alpha1" + kcpversioned "github.com/kcp-dev/kcp/sdk/client/clientset/versioned" + kcpcluster "github.com/kcp-dev/kcp/sdk/client/clientset/versioned/cluster" + kcpinternalinterfaces "github.com/kcp-dev/kcp/sdk/client/informers/externalversions/internalinterfaces" + kcpv1alpha1 "github.com/kcp-dev/kcp/sdk/client/listers/tenancy/v1alpha1" +) + +// WorkspaceAuthenticationConfigurationClusterInformer provides access to a shared informer and lister for +// WorkspaceAuthenticationConfigurations. +type WorkspaceAuthenticationConfigurationClusterInformer interface { + Cluster(logicalcluster.Name) WorkspaceAuthenticationConfigurationInformer + ClusterWithContext(context.Context, logicalcluster.Name) WorkspaceAuthenticationConfigurationInformer + Informer() kcpcache.ScopeableSharedIndexInformer + Lister() kcpv1alpha1.WorkspaceAuthenticationConfigurationClusterLister +} + +type workspaceAuthenticationConfigurationClusterInformer struct { + factory kcpinternalinterfaces.SharedInformerFactory + tweakListOptions kcpinternalinterfaces.TweakListOptionsFunc +} + +// NewWorkspaceAuthenticationConfigurationClusterInformer constructs a new informer for WorkspaceAuthenticationConfiguration type. +// Always prefer using an informer factory to get a shared informer instead of getting an independent +// one. This reduces memory footprint and number of connections to the server. +func NewWorkspaceAuthenticationConfigurationClusterInformer(client kcpcluster.ClusterInterface, resyncPeriod time.Duration, indexers cache.Indexers) kcpcache.ScopeableSharedIndexInformer { + return NewFilteredWorkspaceAuthenticationConfigurationClusterInformer(client, resyncPeriod, indexers, nil) +} + +// NewFilteredWorkspaceAuthenticationConfigurationClusterInformer constructs a new informer for WorkspaceAuthenticationConfiguration type. +// Always prefer using an informer factory to get a shared informer instead of getting an independent +// one. This reduces memory footprint and number of connections to the server. +func NewFilteredWorkspaceAuthenticationConfigurationClusterInformer(client kcpcluster.ClusterInterface, resyncPeriod time.Duration, indexers cache.Indexers, tweakListOptions kcpinternalinterfaces.TweakListOptionsFunc) kcpcache.ScopeableSharedIndexInformer { + return kcpinformers.NewSharedIndexInformer( + &cache.ListWatch{ + ListFunc: func(options v1.ListOptions) (runtime.Object, error) { + if tweakListOptions != nil { + tweakListOptions(&options) + } + return client.TenancyV1alpha1().WorkspaceAuthenticationConfigurations().List(context.Background(), options) + }, + WatchFunc: func(options v1.ListOptions) (watch.Interface, error) { + if tweakListOptions != nil { + tweakListOptions(&options) + } + return client.TenancyV1alpha1().WorkspaceAuthenticationConfigurations().Watch(context.Background(), options) + }, + }, + &kcptenancyv1alpha1.WorkspaceAuthenticationConfiguration{}, + resyncPeriod, + indexers, + ) +} + +func (i *workspaceAuthenticationConfigurationClusterInformer) defaultInformer(client kcpcluster.ClusterInterface, resyncPeriod time.Duration) kcpcache.ScopeableSharedIndexInformer { + return NewFilteredWorkspaceAuthenticationConfigurationClusterInformer(client, resyncPeriod, cache.Indexers{ + kcpcache.ClusterIndexName: kcpcache.ClusterIndexFunc, + kcpcache.ClusterAndNamespaceIndexName: kcpcache.ClusterAndNamespaceIndexFunc, + }, i.tweakListOptions) +} + +func (i *workspaceAuthenticationConfigurationClusterInformer) Informer() kcpcache.ScopeableSharedIndexInformer { + return i.factory.InformerFor(&kcptenancyv1alpha1.WorkspaceAuthenticationConfiguration{}, i.defaultInformer) +} + +func (i *workspaceAuthenticationConfigurationClusterInformer) Lister() kcpv1alpha1.WorkspaceAuthenticationConfigurationClusterLister { + return kcpv1alpha1.NewWorkspaceAuthenticationConfigurationClusterLister(i.Informer().GetIndexer()) +} + +func (i *workspaceAuthenticationConfigurationClusterInformer) Cluster(clusterName logicalcluster.Name) WorkspaceAuthenticationConfigurationInformer { + return &workspaceAuthenticationConfigurationInformer{ + informer: i.Informer().Cluster(clusterName), + lister: i.Lister().Cluster(clusterName), + } +} + +func (i *workspaceAuthenticationConfigurationClusterInformer) ClusterWithContext(ctx context.Context, clusterName logicalcluster.Name) WorkspaceAuthenticationConfigurationInformer { + return &workspaceAuthenticationConfigurationInformer{ + informer: i.Informer().ClusterWithContext(ctx, clusterName), + lister: i.Lister().Cluster(clusterName), + } +} + +type workspaceAuthenticationConfigurationInformer struct { + informer cache.SharedIndexInformer + lister kcpv1alpha1.WorkspaceAuthenticationConfigurationLister +} + +func (i *workspaceAuthenticationConfigurationInformer) Informer() cache.SharedIndexInformer { + return i.informer +} + +func (i *workspaceAuthenticationConfigurationInformer) Lister() kcpv1alpha1.WorkspaceAuthenticationConfigurationLister { + return i.lister +} + +// WorkspaceAuthenticationConfigurationInformer provides access to a shared informer and lister for +// WorkspaceAuthenticationConfigurations. +type WorkspaceAuthenticationConfigurationInformer interface { + Informer() cache.SharedIndexInformer + Lister() kcpv1alpha1.WorkspaceAuthenticationConfigurationLister +} + +type workspaceAuthenticationConfigurationScopedInformer struct { + factory kcpinternalinterfaces.SharedScopedInformerFactory + tweakListOptions kcpinternalinterfaces.TweakListOptionsFunc +} + +// NewWorkspaceAuthenticationConfigurationInformer constructs a new informer for WorkspaceAuthenticationConfiguration type. +// Always prefer using an informer factory to get a shared informer instead of getting an independent +// one. This reduces memory footprint and number of connections to the server. +func NewWorkspaceAuthenticationConfigurationInformer(client kcpversioned.Interface, resyncPeriod time.Duration, indexers cache.Indexers) cache.SharedIndexInformer { + return NewFilteredWorkspaceAuthenticationConfigurationInformer(client, resyncPeriod, indexers, nil) +} + +// NewFilteredWorkspaceAuthenticationConfigurationInformer constructs a new informer for WorkspaceAuthenticationConfiguration type. +// Always prefer using an informer factory to get a shared informer instead of getting an independent +// one. This reduces memory footprint and number of connections to the server. +func NewFilteredWorkspaceAuthenticationConfigurationInformer(client kcpversioned.Interface, resyncPeriod time.Duration, indexers cache.Indexers, tweakListOptions kcpinternalinterfaces.TweakListOptionsFunc) cache.SharedIndexInformer { + return cache.NewSharedIndexInformer( + &cache.ListWatch{ + ListFunc: func(options v1.ListOptions) (runtime.Object, error) { + if tweakListOptions != nil { + tweakListOptions(&options) + } + return client.TenancyV1alpha1().WorkspaceAuthenticationConfigurations().List(context.Background(), options) + }, + WatchFunc: func(options v1.ListOptions) (watch.Interface, error) { + if tweakListOptions != nil { + tweakListOptions(&options) + } + return client.TenancyV1alpha1().WorkspaceAuthenticationConfigurations().Watch(context.Background(), options) + }, + }, + &kcptenancyv1alpha1.WorkspaceAuthenticationConfiguration{}, + resyncPeriod, + indexers, + ) +} + +func (i *workspaceAuthenticationConfigurationScopedInformer) Informer() cache.SharedIndexInformer { + return i.factory.InformerFor(&kcptenancyv1alpha1.WorkspaceAuthenticationConfiguration{}, i.defaultInformer) +} + +func (i *workspaceAuthenticationConfigurationScopedInformer) Lister() kcpv1alpha1.WorkspaceAuthenticationConfigurationLister { + return kcpv1alpha1.NewWorkspaceAuthenticationConfigurationLister(i.Informer().GetIndexer()) +} + +func (i *workspaceAuthenticationConfigurationScopedInformer) defaultInformer(client kcpversioned.Interface, resyncPeriod time.Duration) cache.SharedIndexInformer { + return NewFilteredWorkspaceAuthenticationConfigurationInformer(client, resyncPeriod, cache.Indexers{}, i.tweakListOptions) +} diff --git a/sdk/client/listers/tenancy/v1alpha1/expansion_generated.go b/sdk/client/listers/tenancy/v1alpha1/expansion_generated.go index abd95f372ca..c7ede70e400 100644 --- a/sdk/client/listers/tenancy/v1alpha1/expansion_generated.go +++ b/sdk/client/listers/tenancy/v1alpha1/expansion_generated.go @@ -26,6 +26,14 @@ type WorkspaceClusterListerExpansion interface{} // WorkspaceLister. type WorkspaceListerExpansion interface{} +// WorkspaceAuthenticationConfigurationClusterListerExpansion allows custom methods to be added to +// WorkspaceAuthenticationConfigurationClusterLister. +type WorkspaceAuthenticationConfigurationClusterListerExpansion interface{} + +// WorkspaceAuthenticationConfigurationListerExpansion allows custom methods to be added to +// WorkspaceAuthenticationConfigurationLister. +type WorkspaceAuthenticationConfigurationListerExpansion interface{} + // WorkspaceTypeClusterListerExpansion allows custom methods to be added to // WorkspaceTypeClusterLister. type WorkspaceTypeClusterListerExpansion interface{} diff --git a/sdk/client/listers/tenancy/v1alpha1/workspaceauthenticationconfiguration.go b/sdk/client/listers/tenancy/v1alpha1/workspaceauthenticationconfiguration.go new file mode 100644 index 00000000000..edfbaf69fb6 --- /dev/null +++ b/sdk/client/listers/tenancy/v1alpha1/workspaceauthenticationconfiguration.go @@ -0,0 +1,103 @@ +/* +Copyright The KCP Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Code generated by cluster-lister-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + "k8s.io/apimachinery/pkg/labels" + "k8s.io/client-go/tools/cache" + + kcplisters "github.com/kcp-dev/client-go/third_party/k8s.io/client-go/listers" + "github.com/kcp-dev/logicalcluster/v3" + + kcpv1alpha1 "github.com/kcp-dev/kcp/sdk/apis/tenancy/v1alpha1" +) + +// WorkspaceAuthenticationConfigurationClusterLister helps list WorkspaceAuthenticationConfigurations across all workspaces, +// or scope down to a WorkspaceAuthenticationConfigurationLister for one workspace. +// All objects returned here must be treated as read-only. +type WorkspaceAuthenticationConfigurationClusterLister interface { + // List lists all WorkspaceAuthenticationConfigurations in the indexer. + // Objects returned here must be treated as read-only. + List(selector labels.Selector) (ret []*kcpv1alpha1.WorkspaceAuthenticationConfiguration, err error) + // Cluster returns a lister that can list and get WorkspaceAuthenticationConfigurations in one workspace. + Cluster(clusterName logicalcluster.Name) WorkspaceAuthenticationConfigurationLister + WorkspaceAuthenticationConfigurationClusterListerExpansion +} + +// workspaceAuthenticationConfigurationClusterLister implements the WorkspaceAuthenticationConfigurationClusterLister interface. +type workspaceAuthenticationConfigurationClusterLister struct { + kcplisters.ResourceClusterIndexer[*kcpv1alpha1.WorkspaceAuthenticationConfiguration] +} + +var _ WorkspaceAuthenticationConfigurationClusterLister = new(workspaceAuthenticationConfigurationClusterLister) + +// NewWorkspaceAuthenticationConfigurationClusterLister returns a new WorkspaceAuthenticationConfigurationClusterLister. +// We assume that the indexer: +// - is fed by a cross-workspace LIST+WATCH +// - uses kcpcache.MetaClusterNamespaceKeyFunc as the key function +// - has the kcpcache.ClusterIndex as an index +func NewWorkspaceAuthenticationConfigurationClusterLister(indexer cache.Indexer) WorkspaceAuthenticationConfigurationClusterLister { + return &workspaceAuthenticationConfigurationClusterLister{ + kcplisters.NewCluster[*kcpv1alpha1.WorkspaceAuthenticationConfiguration](indexer, kcpv1alpha1.Resource("workspaceauthenticationconfiguration")), + } +} + +// Cluster scopes the lister to one workspace, allowing users to list and get WorkspaceAuthenticationConfigurations. +func (l *workspaceAuthenticationConfigurationClusterLister) Cluster(clusterName logicalcluster.Name) WorkspaceAuthenticationConfigurationLister { + return &workspaceAuthenticationConfigurationLister{ + l.ResourceClusterIndexer.WithCluster(clusterName), + } +} + +// workspaceAuthenticationConfigurationLister can list all WorkspaceAuthenticationConfigurations inside a workspace +// or scope down to a WorkspaceAuthenticationConfigurationNamespaceLister for one namespace. +type workspaceAuthenticationConfigurationLister struct { + kcplisters.ResourceIndexer[*kcpv1alpha1.WorkspaceAuthenticationConfiguration] +} + +var _ WorkspaceAuthenticationConfigurationLister = new(workspaceAuthenticationConfigurationLister) + +// WorkspaceAuthenticationConfigurationLister can list all WorkspaceAuthenticationConfigurations, or get one in particular. +// All objects returned here must be treated as read-only. +type WorkspaceAuthenticationConfigurationLister interface { + // List lists all WorkspaceAuthenticationConfigurations in the indexer. + // Objects returned here must be treated as read-only. + List(selector labels.Selector) (ret []*kcpv1alpha1.WorkspaceAuthenticationConfiguration, err error) + // Get retrieves the WorkspaceAuthenticationConfiguration from the indexer for a given workspace and name. + // Objects returned here must be treated as read-only. + Get(name string) (*kcpv1alpha1.WorkspaceAuthenticationConfiguration, error) + WorkspaceAuthenticationConfigurationListerExpansion +} + +// NewWorkspaceAuthenticationConfigurationLister returns a new WorkspaceAuthenticationConfigurationLister. +// We assume that the indexer: +// - is fed by a cross-workspace LIST+WATCH +// - uses kcpcache.MetaClusterNamespaceKeyFunc as the key function +// - has the kcpcache.ClusterIndex as an index +func NewWorkspaceAuthenticationConfigurationLister(indexer cache.Indexer) WorkspaceAuthenticationConfigurationLister { + return &workspaceAuthenticationConfigurationLister{ + kcplisters.New[*kcpv1alpha1.WorkspaceAuthenticationConfiguration](indexer, kcpv1alpha1.Resource("workspaceauthenticationconfiguration")), + } +} + +// workspaceAuthenticationConfigurationScopedLister can list all WorkspaceAuthenticationConfigurations inside a workspace +// or scope down to a WorkspaceAuthenticationConfigurationNamespaceLister. +type workspaceAuthenticationConfigurationScopedLister struct { + kcplisters.ResourceIndexer[*kcpv1alpha1.WorkspaceAuthenticationConfiguration] +} diff --git a/sdk/testing/config.go b/sdk/testing/config.go index a87f47bd7e6..fabddbb0094 100644 --- a/sdk/testing/config.go +++ b/sdk/testing/config.go @@ -19,6 +19,8 @@ package testing import ( "sync" + utilfeature "k8s.io/apiserver/pkg/util/feature" + kcptestingserver "github.com/kcp-dev/kcp/sdk/testing/server" ) @@ -30,6 +32,7 @@ var ( sharedConfig = kcptestingserver.Config{ Name: "shared", BindAddress: "127.0.0.1", + Features: utilfeature.DefaultMutableFeatureGate.DeepCopy(), } externalConfig = struct { kubeconfigPath string diff --git a/sdk/testing/kcp.go b/sdk/testing/kcp.go index bca0e83d64e..7d68b6a551d 100644 --- a/sdk/testing/kcp.go +++ b/sdk/testing/kcp.go @@ -23,6 +23,8 @@ import ( "github.com/stretchr/testify/require" + utilfeature "k8s.io/apiserver/pkg/util/feature" + kcptestingserver "github.com/kcp-dev/kcp/sdk/testing/server" "github.com/kcp-dev/kcp/sdk/testing/third_party/library-go/crypto" ) @@ -38,6 +40,7 @@ func PrivateKcpServer(t TestingT, options ...kcptestingserver.Option) kcptesting cfg := &kcptestingserver.Config{ Name: "main", BindAddress: "127.0.0.1", + Features: utilfeature.DefaultMutableFeatureGate.DeepCopy(), } for _, opt := range options { opt(cfg) diff --git a/sdk/testing/server/config.go b/sdk/testing/server/config.go index 47d96c0ec3f..24e527e5b50 100644 --- a/sdk/testing/server/config.go +++ b/sdk/testing/server/config.go @@ -21,7 +21,7 @@ import ( "path/filepath" "strconv" - utilfeature "k8s.io/apiserver/pkg/util/feature" + "k8s.io/component-base/featuregate" ) // Config qualify a kcp server to start @@ -34,6 +34,7 @@ type Config struct { DataDir string ClientCADir string BindAddress string + Features featuregate.MutableFeatureGate LogToConsole bool RunInProcess bool @@ -64,11 +65,14 @@ func (c Config) BuildArgs(t TestingT) ([]string, error) { "--embedded-etcd-peer-port=" + etcdPeerPort, "--embedded-etcd-wal-size-bytes=" + strconv.Itoa(5*1000), // 5KB "--kubeconfig-path=" + c.KubeconfigPath(), - "--feature-gates=" + fmt.Sprintf("%s", utilfeature.DefaultFeatureGate), "--audit-log-path", filepath.Join(c.ArtifactDir, "kcp.audit"), "--v=4", } + if c.Features != nil { + args = append(args, "--feature-gates="+fmt.Sprintf("%s", c.Features)) + } + if c.BindAddress != "" { args = append(args, "--bind-address="+c.BindAddress) } @@ -105,6 +109,19 @@ func WithCustomArguments(args ...string) Option { } } +// WithFeatures configures one or more features. +func WithFeatures(m map[string]bool) Option { + return func(cfg *Config) { + if cfg.Features == nil { + cfg.Features = featuregate.NewFeatureGate() + } + + if err := cfg.Features.SetFromMap(m); err != nil { + panic(fmt.Sprintf("Failed to set features: %v", err)) + } + } +} + // WithClientCA sets the client CA directory for a given kcp configuration. // A client CA will automatically created and the --client-ca configured. func WithClientCA(clientCADir string) Option { diff --git a/test/e2e/authentication/mockoidc.go b/test/e2e/authentication/mockoidc.go new file mode 100644 index 00000000000..d731cb34d4a --- /dev/null +++ b/test/e2e/authentication/mockoidc.go @@ -0,0 +1,161 @@ +/* +Copyright 2025 The KCP Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package authentication + +import ( + "crypto/tls" + "crypto/x509" + "net" + "path/filepath" + "sync" + "testing" + + "github.com/golang-jwt/jwt/v5" + "github.com/stretchr/testify/require" + "github.com/xrstf/mockoidc" + + "k8s.io/apimachinery/pkg/util/sets" + "k8s.io/utils/ptr" + + tenancyv1alpha1 "github.com/kcp-dev/kcp/sdk/apis/tenancy/v1alpha1" + kcptestingserver "github.com/kcp-dev/kcp/sdk/testing/server" + "github.com/kcp-dev/kcp/sdk/testing/third_party/library-go/crypto" +) + +func startMockOIDC(t *testing.T, server kcptestingserver.RunningServer) (*mockoidc.MockOIDC, *crypto.CA) { + // start a mock OIDC server that will listen on a random port + // (only for discovery and keyset handling, no actual login workflows) + caDir := server.CADirectory() + caCertFile := filepath.Join(caDir, "mockoidc-ca.crt") + caKeyFile := filepath.Join(caDir, "mockoidc-ca.key") + + ca, _, err := crypto.EnsureCA(caCertFile, caKeyFile, "", "mockoidc-ca", 1) + require.NoError(t, err) + + caPool := x509.NewCertPool() + caPool.AddCert(ca.Config.Certs[0]) + + cert, err := ca.MakeServerCert(sets.New("localhost"), 30, func(c *x509.Certificate) error { + c.IPAddresses = []net.IP{ + net.ParseIP("127.0.0.1"), + } + return nil + }) + require.NoError(t, err) + + tlsCert := tls.Certificate{ + Certificate: [][]byte{}, + PrivateKey: cert.Key, + } + + for _, c := range cert.Certs { + tlsCert.Certificate = append(tlsCert.Certificate, c.Raw) + } + + tlsConfig := &tls.Config{ + RootCAs: caPool, + ServerName: "localhost", + Certificates: []tls.Certificate{tlsCert}, + } + + m, err := mockoidc.RunTLS(tlsConfig) + require.NoError(t, err) + + // Since most tests run in parallel, the main test will end sooner than the subtests and so + // if we'd use `defer m.Shutdown()`, that defer would run before the subtests are even executed. + // t.Cleanup() is a bit more picky and to avoid repetitive closures, this function sets up the + // necessary shutdown code already. + t.Cleanup(func() { + require.NoError(t, m.Shutdown()) + }) + + return m, ca +} + +func mockJWTAuthenticator(t *testing.T, m *mockoidc.MockOIDC, ca *crypto.CA, userPrefix, groupPrefix string) tenancyv1alpha1.JWTAuthenticator { + cfg := m.Config() + + caCert, _, err := ca.Config.GetPEMBytes() + require.NoError(t, err) + + return tenancyv1alpha1.JWTAuthenticator{ + Issuer: tenancyv1alpha1.Issuer{ + URL: cfg.Issuer, + Audiences: []string{cfg.ClientID}, + CertificateAuthority: string(caCert), + }, + ClaimMappings: tenancyv1alpha1.ClaimMappings{ + Username: tenancyv1alpha1.PrefixedClaimOrExpression{ + Claim: "email", + Prefix: ptr.To(userPrefix), + }, + Groups: tenancyv1alpha1.PrefixedClaimOrExpression{ + Claim: "groups", + Prefix: ptr.To(groupPrefix), + }, + }, + } +} + +var tokenLock sync.Mutex + +func createOIDCToken(t *testing.T, mock *mockoidc.MockOIDC, subject, email string, groups []string) string { + var ( + cfg = mock.Config() + now = mockoidc.NowFunc() + scope = "openid groups email" + nonce = "noften" + codeChallenge = "nothing-to-see-here" + codeChallengeMethod = cfg.CodeChallengeMethodsSupported[0] + ) + + // NewSession() is not concurrency safe, but we want to allow parallel tests. + tokenLock.Lock() + defer tokenLock.Unlock() + + session, err := mock.SessionStore.NewSession(scope, nonce, &mockoidc.MockUser{ + Subject: subject, + Email: email, + Groups: groups, + }, codeChallenge, codeChallengeMethod) + require.NoError(t, err) + + // session.IDToken does not allow to customize the audience claim, so we have to build it by hand. + // To access kcp, a token needs to have both the kcp audience and match the audience configured + // in the WorkspaceAuthConfig object. + claims, err := session.User.Claims(session.Scopes, &mockoidc.IDTokenClaims{ + RegisteredClaims: &jwt.RegisteredClaims{ + Audience: jwt.ClaimStrings{ + cfg.ClientID, + "https://kcp.default.svc", + }, + ExpiresAt: jwt.NewNumericDate(now.Add(cfg.AccessTTL)), + ID: session.SessionID, + IssuedAt: jwt.NewNumericDate(now), + Issuer: cfg.Issuer, + NotBefore: jwt.NewNumericDate(now), + Subject: session.User.ID(), + }, + Nonce: nonce, + }) + require.NoError(t, err) + + token, err := mock.Keypair.SignJWT(claims) + require.NoError(t, err) + + return token +} diff --git a/test/e2e/authentication/workspace_test.go b/test/e2e/authentication/workspace_test.go new file mode 100644 index 00000000000..eca7238273e --- /dev/null +++ b/test/e2e/authentication/workspace_test.go @@ -0,0 +1,486 @@ +/* +Copyright 2025 The KCP Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package authentication + +import ( + "context" + "fmt" + "math/rand" + "testing" + "time" + + "github.com/stretchr/testify/require" + "github.com/xrstf/mockoidc" + + authenticationv1 "k8s.io/api/authentication/v1" + rbacv1 "k8s.io/api/rbac/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/util/wait" + + kcpkubernetesclientset "github.com/kcp-dev/client-go/kubernetes" + "github.com/kcp-dev/logicalcluster/v3" + + "github.com/kcp-dev/kcp/pkg/authorization/bootstrap" + tenancyv1alpha1 "github.com/kcp-dev/kcp/sdk/apis/tenancy/v1alpha1" + kcpclientset "github.com/kcp-dev/kcp/sdk/client/clientset/versioned/cluster" + kcptesting "github.com/kcp-dev/kcp/sdk/testing" + "github.com/kcp-dev/kcp/sdk/testing/third_party/library-go/crypto" + "github.com/kcp-dev/kcp/test/e2e/framework" +) + +func TestWorkspaceOIDC(t *testing.T) { + framework.Suite(t, "control-plane") + + ctx := context.Background() + + // start kcp and setup clients + server := kcptesting.SharedKcpServer(t) + + baseWsPath, _ := kcptesting.NewWorkspaceFixture(t, server, logicalcluster.NewPath("root"), kcptesting.WithNamePrefix("oidc")) + + kcpConfig := server.BaseConfig(t) + kubeClusterClient, err := kcpkubernetesclientset.NewForConfig(kcpConfig) + require.NoError(t, err) + kcpClusterClient, err := kcpclientset.NewForConfig(kcpConfig) + require.NoError(t, err) + + // start a two mock OIDC servers that will listen on random ports + // (only for discovery and keyset handling, no actual login workflows) + mockA, ca := startMockOIDC(t, server) + mockB, _ := startMockOIDC(t, server) + + // setup a new workspace auth config that uses mockoidc's server, one for + // each of our mockoidc servers + authConfigA := createWorkspaceAuthentication(t, ctx, kcpClusterClient, baseWsPath, mockA, ca) + authConfigB := createWorkspaceAuthentication(t, ctx, kcpClusterClient, baseWsPath, mockB, ca) + + // use these configs in new WorkspaceTypes and create one extra workspace type that allows + // both mockoidc issuers + wsTypeA := createWorkspaceType(t, ctx, kcpClusterClient, baseWsPath, authConfigA) + wsTypeB := createWorkspaceType(t, ctx, kcpClusterClient, baseWsPath, authConfigB) + wsTypeC := createWorkspaceType(t, ctx, kcpClusterClient, baseWsPath, authConfigA, authConfigB) + + // create a new workspace with our new type + t.Log("Creating Workspaces...") + teamAPath, _ := kcptesting.NewWorkspaceFixture(t, server, baseWsPath, kcptesting.WithName("team-a"), kcptesting.WithType(baseWsPath, tenancyv1alpha1.WorkspaceTypeName(wsTypeA))) + teamBPath, _ := kcptesting.NewWorkspaceFixture(t, server, baseWsPath, kcptesting.WithName("team-b"), kcptesting.WithType(baseWsPath, tenancyv1alpha1.WorkspaceTypeName(wsTypeB))) + teamCPath, _ := kcptesting.NewWorkspaceFixture(t, server, baseWsPath, kcptesting.WithName("team-c"), kcptesting.WithType(baseWsPath, tenancyv1alpha1.WorkspaceTypeName(wsTypeC))) + + // sanity check: owner can access their own workspaces + for _, path := range []logicalcluster.Path{teamAPath, teamBPath, teamCPath} { + t.Logf("The workspace owner should be allowed to list ConfigMaps in %s...", path) + _, err = kubeClusterClient.Cluster(path).CoreV1().ConfigMaps("default").List(ctx, metav1.ListOptions{}) + require.NoError(t, err) + } + + // grant permissions to random users and groups + grantWorkspaceAccess(t, ctx, kubeClusterClient, teamAPath, []rbacv1.Subject{{ + Kind: "User", + Name: "oidc:user-a@example.com", + }, { + Kind: "Group", + Name: "oidc:developers", + }}) + + grantWorkspaceAccess(t, ctx, kubeClusterClient, teamBPath, []rbacv1.Subject{{ + Kind: "User", + Name: "oidc:user-b@example.com", + }, { + Kind: "Group", + Name: "oidc:testers", + }, { + Kind: "Group", + Name: "oidc:developers", + }}) + + grantWorkspaceAccess(t, ctx, kubeClusterClient, teamCPath, []rbacv1.Subject{{ + Kind: "User", + Name: "oidc:user-c@example.com", + }, { + Kind: "Group", + Name: "oidc:developers", + }}) + + // random user with random token should have no access + randoKubeClusterClient, err := kcpkubernetesclientset.NewForConfig(framework.ConfigWithToken("invalid-token", kcpConfig)) + require.NoError(t, err) + + for _, path := range []logicalcluster.Path{teamAPath, teamBPath, teamCPath} { + t.Logf("An unauthenticated user shouldn't be able to list ConfigMaps in %s...", path) + + _, err = randoKubeClusterClient.Cluster(path).CoreV1().ConfigMaps("default").List(ctx, metav1.ListOptions{}) + require.Error(t, err) + } + + testcases := []struct { + name string + username string + email string + groups []string + mock *mockoidc.MockOIDC + workspaceAccess map[logicalcluster.Path]bool + }{ + { + name: "user-a@example.com should be able to access workspace A only", + username: "user-a", + email: "user-a@example.com", + groups: nil, + mock: mockA, + workspaceAccess: map[logicalcluster.Path]bool{ + teamAPath: true, + teamBPath: false, + teamCPath: false, // user is authenticated but has no permissions in this workspace + }, + }, + { + name: "user-b@example.com should be able to access workspace B only", + username: "user-b", + email: "user-b@example.com", + groups: nil, + mock: mockB, + workspaceAccess: map[logicalcluster.Path]bool{ + teamAPath: false, + teamBPath: true, + teamCPath: false, // user is authenticated but has no permissions in this workspace + }, + }, + { + name: "user-a@example.com (developers) should be able to access workspace A and C", + username: "user-a", + email: "user-a@example.com", + groups: []string{"developers"}, + mock: mockA, + workspaceAccess: map[logicalcluster.Path]bool{ + teamAPath: true, + teamBPath: false, + teamCPath: true, + }, + }, + { + name: "Any user in the developers group should be able to access workspace A and C", + username: "random-user", + email: "rando@example.com", + groups: []string{"developers"}, + mock: mockA, + workspaceAccess: map[logicalcluster.Path]bool{ + teamAPath: true, + teamBPath: false, // false is correct since B does allow developers in, but only from its own issuer! + teamCPath: true, + }, + }, + { + name: "User C, signed by issuer A, is allowed to access workspace C", + username: "user-c", + email: "user-c@example.com", + groups: nil, + mock: mockA, + workspaceAccess: map[logicalcluster.Path]bool{ + teamAPath: false, + teamBPath: false, + teamCPath: true, + }, + }, + { + name: "User C, signed by issuer B, is allowed to access workspace C", + username: "user-c", + email: "user-c@example.com", + groups: nil, + mock: mockB, + workspaceAccess: map[logicalcluster.Path]bool{ + teamAPath: false, + teamBPath: false, + teamCPath: true, + }, + }, + { + name: "impersonating system groups is not allowed and those groups will be stripped", + username: "hacker", + email: "hacker@1337code.no", + groups: []string{bootstrap.SystemKcpAdminGroup, "system:masters"}, + mock: mockB, + workspaceAccess: map[logicalcluster.Path]bool{ + teamAPath: false, + teamBPath: false, + teamCPath: false, + }, + }, + } + + for _, testcase := range testcases { + t.Run(testcase.name, func(t *testing.T) { + t.Parallel() + + token := createOIDCToken(t, testcase.mock, testcase.username, testcase.email, testcase.groups) + + client, err := kcpkubernetesclientset.NewForConfig(framework.ConfigWithToken(token, kcpConfig)) + require.NoError(t, err) + + for workspace, expectedResult := range testcase.workspaceAccess { + t.Run(workspace.Base(), func(t *testing.T) { + t.Parallel() + + if expectedResult { + // Initialization of the JWT authenticator happens asynchronously and depends both + // on the cluster index knowing the WorkspaceType and the authenticator itself + // performing OIDC discovery. + + require.Eventually(t, func() bool { + _, err := client.Cluster(workspace).CoreV1().ConfigMaps("default").List(ctx, metav1.ListOptions{}) + + return err == nil + }, wait.ForeverTestTimeout, 500*time.Millisecond) + } else { + _, err := client.Cluster(workspace).CoreV1().ConfigMaps("default").List(ctx, metav1.ListOptions{}) + require.Error(t, err, "user should have no access") + } + }) + } + }) + } +} + +func TestUserScope(t *testing.T) { + framework.Suite(t, "control-plane") + + ctx := context.Background() + + // start kcp and setup clients + server := kcptesting.SharedKcpServer(t) + + baseWsPath, _ := kcptesting.NewWorkspaceFixture(t, server, logicalcluster.NewPath("root"), kcptesting.WithNamePrefix("oidc-scope")) + + kcpConfig := server.BaseConfig(t) + kubeClusterClient, err := kcpkubernetesclientset.NewForConfig(kcpConfig) + require.NoError(t, err) + kcpClusterClient, err := kcpclientset.NewForConfig(kcpConfig) + require.NoError(t, err) + + mock, ca := startMockOIDC(t, server) + authConfig := createWorkspaceAuthentication(t, ctx, kcpClusterClient, baseWsPath, mock, ca) + wsType := createWorkspaceType(t, ctx, kcpClusterClient, baseWsPath, authConfig) + + // create a new workspace with our new type + t.Log("Creating Workspaces...") + teamPath, teamWs := kcptesting.NewWorkspaceFixture(t, server, baseWsPath, kcptesting.WithName("team-a"), kcptesting.WithType(baseWsPath, tenancyv1alpha1.WorkspaceTypeName(wsType))) + + var ( + userName = "peter" + userEmail = "peter@example.com" + userGroups = []string{"developers", "admins"} + expectedGroups = []string{} + ) + + for _, group := range userGroups { + expectedGroups = append(expectedGroups, "oidc:"+group) + } + + grantWorkspaceAccess(t, ctx, kubeClusterClient, teamPath, []rbacv1.Subject{{ + Kind: "User", + Name: "oidc:" + userEmail, + }}) + + token := createOIDCToken(t, mock, userName, userEmail, userGroups) + + peterClient, err := kcpkubernetesclientset.NewForConfig(framework.ConfigWithToken(token, kcpConfig)) + require.NoError(t, err) + + t.Logf("Creating SelfSubjectAccessReview in %s", teamPath) + + var review *authenticationv1.SelfSubjectReview + require.Eventually(t, func() bool { + request := &authenticationv1.SelfSubjectReview{} + + var err error + review, err = peterClient.Cluster(teamPath).AuthenticationV1().SelfSubjectReviews().Create(ctx, request, metav1.CreateOptions{}) + if err != nil { + t.Log(err) + } + + return err == nil + }, wait.ForeverTestTimeout, 500*time.Millisecond) + + user := review.Status.UserInfo + require.Equal(t, "oidc:"+userEmail, user.Username) + require.Subset(t, user.Groups, expectedGroups) + require.Equal(t, user.Extra["authentication.kcp.io/scopes"], authenticationv1.ExtraValue{"cluster:" + teamWs.Spec.Cluster}) +} + +func TestForbiddenSystemAccess(t *testing.T) { + framework.Suite(t, "control-plane") + + ctx := context.Background() + + // start kcp and setup clients + server := kcptesting.SharedKcpServer(t) + + baseWsPath, _ := kcptesting.NewWorkspaceFixture(t, server, logicalcluster.NewPath("root"), kcptesting.WithNamePrefix("oidc-scope")) + + kcpConfig := server.BaseConfig(t) + kubeClusterClient, err := kcpkubernetesclientset.NewForConfig(kcpConfig) + require.NoError(t, err) + kcpClusterClient, err := kcpclientset.NewForConfig(kcpConfig) + require.NoError(t, err) + + mock, ca := startMockOIDC(t, server) + + // create an evil AuthConfig that would not prefix OIDC-provided groups, theoretically allowing + // users to become part of system groups. + // setup a new workspace auth config that uses mockoidc's server + authConfig := &tenancyv1alpha1.WorkspaceAuthenticationConfiguration{ + ObjectMeta: metav1.ObjectMeta{ + Name: "evil-oidc", + }, + Spec: tenancyv1alpha1.WorkspaceAuthenticationConfigurationSpec{ + JWT: []tenancyv1alpha1.JWTAuthenticator{ + mockJWTAuthenticator(t, mock, ca, "", ""), + }, + }, + } + + t.Logf("Creating WorkspaceAuthenticationConfguration %s...", authConfig.Name) + _, err = kcpClusterClient.Cluster(baseWsPath).TenancyV1alpha1().WorkspaceAuthenticationConfigurations().Create(ctx, authConfig, metav1.CreateOptions{}) + require.NoError(t, err) + + wsType := createWorkspaceType(t, ctx, kcpClusterClient, baseWsPath, authConfig.Name) + + // create a new workspace with our new type + t.Log("Creating Workspaces...") + teamPath, _ := kcptesting.NewWorkspaceFixture(t, server, baseWsPath, kcptesting.WithName("team-a"), kcptesting.WithType(baseWsPath, tenancyv1alpha1.WorkspaceTypeName(wsType))) + + // give a dummy user access + grantWorkspaceAccess(t, ctx, kubeClusterClient, teamPath, []rbacv1.Subject{{ + Kind: "User", + Name: "dummy@example.com", + }}) + + // wait until the authenticator is ready + token := createOIDCToken(t, mock, "dummy", "dummy@example.com", nil) + + client, err := kcpkubernetesclientset.NewForConfig(framework.ConfigWithToken(token, kcpConfig)) + require.NoError(t, err) + + t.Log("Waiting for authenticator to be ready...") + require.Eventually(t, func() bool { + _, err := client.Cluster(teamPath).CoreV1().ConfigMaps("default").List(ctx, metav1.ListOptions{}) + + return err == nil + }, wait.ForeverTestTimeout, 500*time.Millisecond) + + // Now that we know that the authenticator is ready, run the actual tests that ensure we do NOT + // gain access based on our system names / groups. + + testcases := []struct { + name string + username string + email string + groups []string + }{ + { + name: fmt.Sprintf("%s should not give workspace access", bootstrap.SystemKcpAdminGroup), + username: "al", + email: "al@bundy.com", + groups: []string{bootstrap.SystemKcpAdminGroup}, + }, + { + name: "shard-admin should not be admitted", + username: "al", + email: "shard-admin", + groups: nil, + }, + } + + t.Log("Testing tokens...") + for _, testcase := range testcases { + t.Run(testcase.name, func(t *testing.T) { + t.Parallel() + + token := createOIDCToken(t, mock, testcase.username, testcase.email, testcase.groups) + + client, err := kcpkubernetesclientset.NewForConfig(framework.ConfigWithToken(token, kcpConfig)) + require.NoError(t, err) + + _, err = client.Cluster(teamPath).CoreV1().ConfigMaps("default").List(ctx, metav1.ListOptions{}) + require.Error(t, err, "user should have no access") + }) + } +} + +func createWorkspaceAuthentication(t *testing.T, ctx context.Context, client kcpclientset.ClusterInterface, workspace logicalcluster.Path, mock *mockoidc.MockOIDC, ca *crypto.CA) string { + name := fmt.Sprintf("mockoidc-%d", rand.Int()) + + // setup a new workspace auth config that uses mockoidc's server + authConfig := &tenancyv1alpha1.WorkspaceAuthenticationConfiguration{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + }, + Spec: tenancyv1alpha1.WorkspaceAuthenticationConfigurationSpec{ + JWT: []tenancyv1alpha1.JWTAuthenticator{ + mockJWTAuthenticator(t, mock, ca, "oidc:", "oidc:"), + }, + }, + } + + t.Logf("Creating WorkspaceAuthenticationConfguration %s...", name) + _, err := client.Cluster(workspace).TenancyV1alpha1().WorkspaceAuthenticationConfigurations().Create(ctx, authConfig, metav1.CreateOptions{}) + require.NoError(t, err) + + return name +} + +func createWorkspaceType(t *testing.T, ctx context.Context, client kcpclientset.ClusterInterface, workspace logicalcluster.Path, authConfigNames ...string) string { + name := fmt.Sprintf("with-oidc-%d", rand.Int()) + + configs := []tenancyv1alpha1.AuthenticationConfigurationReference{} + for _, name := range authConfigNames { + configs = append(configs, tenancyv1alpha1.AuthenticationConfigurationReference{ + Name: name, + }) + } + + // setup a new workspace auth config that uses mockoidc's server + wsType := &tenancyv1alpha1.WorkspaceType{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + }, + Spec: tenancyv1alpha1.WorkspaceTypeSpec{ + AuthenticationConfigurations: configs, + }, + } + + t.Logf("Creating WorkspaceType %s...", name) + _, err := client.Cluster(workspace).TenancyV1alpha1().WorkspaceTypes().Create(ctx, wsType, metav1.CreateOptions{}) + require.NoError(t, err) + + return name +} + +func grantWorkspaceAccess(t *testing.T, ctx context.Context, client kcpkubernetesclientset.ClusterInterface, workspace logicalcluster.Path, subjects []rbacv1.Subject) { + crb := &rbacv1.ClusterRoleBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: "allow-oidc-user", + }, + RoleRef: rbacv1.RoleRef{ + Kind: "ClusterRole", + Name: "cluster-admin", + }, + Subjects: subjects, + } + + t.Log("Creating ClusterRoleBinding...") + _, err := client.Cluster(workspace).RbacV1().ClusterRoleBindings().Create(ctx, crb, metav1.CreateOptions{}) + require.NoError(t, err) +} diff --git a/test/e2e/framework/kcp.go b/test/e2e/framework/kcp.go index 8e613f8a0eb..976a530969f 100644 --- a/test/e2e/framework/kcp.go +++ b/test/e2e/framework/kcp.go @@ -30,7 +30,7 @@ var DefaultTokenAuthFile = filepath.Join(kcptestinghelpers.RepositoryDir(), "tes func init() { var args []string args = append(args, "--token-auth-file", DefaultTokenAuthFile) //nolint:gocritic // no. - args = append(args, "--feature-gates=WorkspaceMounts=true,CacheAPIs=true") + args = append(args, "--feature-gates=WorkspaceMounts=true,CacheAPIs=true,WorkspaceAuthentication=true") kcptesting.InitSharedKcpServer(kcptestingserver.WithCustomArguments(args...)) }