diff --git a/.github/dependabot.yml b/.github/dependabot.yml index a5bf10208..9af0d2353 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -9,12 +9,25 @@ updates: schedule: interval: "daily" -# Our own CI job is responsible for updating this go.mod file now. -# - package-ecosystem: "gomod" -# open-pull-requests-limit: 100 -# directory: "/" -# schedule: -# interval: "daily" + # Use dependabot to automate major-only dependency bumps + - package-ecosystem: "gomod" + open-pull-requests-limit: 2 # Not sure why there would ever be more than 1, just would not want to hide anything + directory: "/" + schedule: + interval: "daily" + # group all major dependency bumps together so there's only one pull request + groups: + go-modules: + patterns: + - "*" + update-types: + - "major" + ignore: + # For all packages, ignore all minor and patch updates + - dependency-name: "*" + update-types: + - "version-update:semver-minor" + - "version-update:semver-patch" # Our own CI job is responsible for updating this Docker file now. # - package-ecosystem: "docker" diff --git a/Dockerfile b/Dockerfile index fa5eb6296..d4f96dc53 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,9 +1,9 @@ # syntax=docker/dockerfile:1 -# Copyright 2020-2023 the Pinniped contributors. All Rights Reserved. +# Copyright 2020-2024 the Pinniped contributors. All Rights Reserved. # SPDX-License-Identifier: Apache-2.0 -ARG BUILD_IMAGE=golang:1.22.3@sha256:f43c6f049f04cbbaeb28f0aad3eea15274a7d0a7899a617d0037aec48d7ab010 +ARG BUILD_IMAGE=golang:1.22.4@sha256:969349b8121a56d51c74f4c273ab974c15b3a8ae246a5cffc1df7d28b66cf978 ARG BASE_IMAGE=gcr.io/distroless/static:nonroot@sha256:e9ac71e2b8e279a8372741b7a0293afda17650d926900233ec3a7b2b7c22a246 # Prepare to cross-compile by always running the build stage in the build platform, not the target platform. diff --git a/apis/supervisor/idp/v1alpha1/register.go.tmpl b/apis/supervisor/idp/v1alpha1/register.go.tmpl index 8829a8638..705be8076 100644 --- a/apis/supervisor/idp/v1alpha1/register.go.tmpl +++ b/apis/supervisor/idp/v1alpha1/register.go.tmpl @@ -1,4 +1,4 @@ -// Copyright 2020-2022 the Pinniped contributors. All Rights Reserved. +// Copyright 2020-2024 the Pinniped contributors. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 package v1alpha1 @@ -36,6 +36,8 @@ func addKnownTypes(scheme *runtime.Scheme) error { &LDAPIdentityProviderList{}, &ActiveDirectoryIdentityProvider{}, &ActiveDirectoryIdentityProviderList{}, + &GitHubIdentityProvider{}, + &GitHubIdentityProviderList{}, ) metav1.AddToGroupVersion(scheme, SchemeGroupVersion) return nil diff --git a/apis/supervisor/idp/v1alpha1/types_githubidentityprovider.go.tmpl b/apis/supervisor/idp/v1alpha1/types_githubidentityprovider.go.tmpl new file mode 100644 index 000000000..c84f46dbd --- /dev/null +++ b/apis/supervisor/idp/v1alpha1/types_githubidentityprovider.go.tmpl @@ -0,0 +1,256 @@ +// Copyright 2024 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package v1alpha1 + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +type GitHubIdentityProviderPhase string + +const ( + // GitHubPhasePending is the default phase for newly-created GitHubIdentityProvider resources. + GitHubPhasePending GitHubIdentityProviderPhase = "Pending" + + // GitHubPhaseReady is the phase for an GitHubIdentityProvider resource in a healthy state. + GitHubPhaseReady GitHubIdentityProviderPhase = "Ready" + + // GitHubPhaseError is the phase for an GitHubIdentityProvider in an unhealthy state. + GitHubPhaseError GitHubIdentityProviderPhase = "Error" +) + +type GitHubAllowedAuthOrganizationsPolicy string + +const ( + // GitHubAllowedAuthOrganizationsPolicyAllGitHubUsers means any GitHub user is allowed to log in using this identity + // provider, regardless of their organization membership or lack thereof. + GitHubAllowedAuthOrganizationsPolicyAllGitHubUsers GitHubAllowedAuthOrganizationsPolicy = "AllGitHubUsers" + + // GitHubAllowedAuthOrganizationsPolicyOnlyUsersFromAllowedOrganizations means only those users with membership in + // the listed GitHub organizations are allowed to log in. + GitHubAllowedAuthOrganizationsPolicyOnlyUsersFromAllowedOrganizations GitHubAllowedAuthOrganizationsPolicy = "OnlyUsersFromAllowedOrganizations" +) + +// GitHubIdentityProviderStatus is the status of an GitHub identity provider. +type GitHubIdentityProviderStatus struct { + // Phase summarizes the overall status of the GitHubIdentityProvider. + // + // +kubebuilder:default=Pending + // +kubebuilder:validation:Enum=Pending;Ready;Error + Phase GitHubIdentityProviderPhase `json:"phase,omitempty"` + + // Conditions represents the observations of an identity provider's current state. + // + // +patchMergeKey=type + // +patchStrategy=merge + // +listType=map + // +listMapKey=type + Conditions []metav1.Condition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type"` +} + +// GitHubAPIConfig allows configuration for GitHub Enterprise Server +type GitHubAPIConfig struct { + // Host is required only for GitHub Enterprise Server. + // Defaults to using GitHub's public API ("github.com"). + // Do not specify a protocol or scheme since "https://" will always be used. + // Port is optional. Do not specify a path, query, fragment, or userinfo. + // Only domain name or IP address, subdomains (optional), and port (optional). + // IPv4 and IPv6 are supported. If using an IPv6 address with a port, you must enclose the IPv6 address + // in square brackets. Example: "[::1]:443". + // + // +kubebuilder:default="github.com" + // +kubebuilder:validation:MinLength=1 + // +optional + Host *string `json:"host"` + + // TLS configuration for GitHub Enterprise Server. + // + // +optional + TLS *TLSSpec `json:"tls,omitempty"` +} + +// GitHubUsernameAttribute allows the user to specify which attribute(s) from GitHub to use for the username to present +// to Kubernetes. See the response schema for +// [Get the authenticated user](https://docs.github.com/en/rest/users/users?apiVersion=2022-11-28#get-the-authenticated-user). +type GitHubUsernameAttribute string + +const ( + // GitHubUsernameID specifies using the `id` attribute from the GitHub user for the username to present to Kubernetes. + GitHubUsernameID GitHubUsernameAttribute = "id" + + // GitHubUsernameLogin specifies using the `login` attribute from the GitHub user as the username to present to Kubernetes. + GitHubUsernameLogin GitHubUsernameAttribute = "login" + + // GitHubUsernameLoginAndID specifies combining the `login` and `id` attributes from the GitHub user as the + // username to present to Kubernetes, separated by a colon. Example: "my-login:1234" + GitHubUsernameLoginAndID GitHubUsernameAttribute = "login:id" +) + +// GitHubGroupNameAttribute allows the user to specify which attribute from GitHub to use for the group +// names to present to Kubernetes. See the response schema for +// [List teams for the authenticated user](https://docs.github.com/en/rest/teams/teams?apiVersion=2022-11-28#list-teams-for-the-authenticated-user). +type GitHubGroupNameAttribute string + +const ( + // GitHubUseTeamNameForGroupName specifies using the GitHub team's `name` attribute as the group name to present to Kubernetes. + GitHubUseTeamNameForGroupName GitHubGroupNameAttribute = "name" + + // GitHubUseTeamSlugForGroupName specifies using the GitHub team's `slug` attribute as the group name to present to Kubernetes. + GitHubUseTeamSlugForGroupName GitHubGroupNameAttribute = "slug" +) + +// GitHubClaims allows customization of the username and groups claims. +type GitHubClaims struct { + // Username configures which property of the GitHub user record shall determine the username in Kubernetes. + // + // Can be either "id", "login", or "login:id". Defaults to "login:id". + // + // GitHub's user login attributes can only contain alphanumeric characters and non-repeating hyphens, + // and may not start or end with hyphens. GitHub users are allowed to change their login name, + // although it is inconvenient. If a GitHub user changed their login name from "foo" to "bar", + // then a second user might change their name from "baz" to "foo" in order to take the old + // username of the first user. For this reason, it is not as safe to make authorization decisions + // based only on the user's login attribute. + // + // If desired, an admin could configure identity transformation expressions on the Pinniped Supervisor's + // FederationDomain to further customize how these usernames are presented to Kubernetes. + // + // Defaults to "login:id", which is the user login attribute, followed by a colon, followed by the unique and + // unchanging integer ID number attribute. This blends human-readable login names with the unchanging ID value + // from GitHub. Colons are not allowed in GitHub login attributes or ID numbers, so this is a reasonable + // choice to concatenate the two values. + // + // See the response schema for + // [Get the authenticated user](https://docs.github.com/en/rest/users/users?apiVersion=2022-11-28#get-the-authenticated-user). + // + // +kubebuilder:default="login:id" + // +kubebuilder:validation:Enum={"id","login","login:id"} + // +optional + Username *GitHubUsernameAttribute `json:"username"` + + // Groups configures which property of the GitHub team record shall determine the group names in Kubernetes. + // + // Can be either "name" or "slug". Defaults to "slug". + // + // GitHub team names can contain upper and lower case characters, whitespace, and punctuation (e.g. "Kube admins!"). + // + // GitHub team slugs are lower case alphanumeric characters and may contain dashes and underscores (e.g. "kube-admins"). + // + // Group names as presented to Kubernetes will always be prefixed by the GitHub organization name followed by a + // forward slash (e.g. "my-org/my-team"). GitHub organization login names can only contain alphanumeric characters + // or single hyphens, so the first forward slash `/` will be the separator between the organization login name and + // the team name or slug. + // + // If desired, an admin could configure identity transformation expressions on the Pinniped Supervisor's + // FederationDomain to further customize how these group names are presented to Kubernetes. + // + // See the response schema for + // [List teams for the authenticated user](https://docs.github.com/en/rest/teams/teams?apiVersion=2022-11-28#list-teams-for-the-authenticated-user). + // + // +kubebuilder:default=slug + // +kubebuilder:validation:Enum=name;slug + // +optional + Groups *GitHubGroupNameAttribute `json:"groups"` +} + +// GitHubClientSpec contains information about the GitHub client that this identity provider will use +// for web-based login flows. +type GitHubClientSpec struct { + // SecretName contains the name of a namespace-local Secret object that provides the clientID and + // clientSecret for an GitHub App or GitHub OAuth2 client. + // + // This secret must be of type "secrets.pinniped.dev/github-client" with keys "clientID" and "clientSecret". + // + // +kubebuilder:validation:MinLength=1 + SecretName string `json:"secretName"` +} + +type GitHubOrganizationsSpec struct { + // Policy must be set to "AllGitHubUsers" if allowed is empty. + // + // This field only exists to ensure that Pinniped administrators are aware that an empty list of + // allowedOrganizations means all GitHub users are allowed to log in. + // + // +kubebuilder:default=OnlyUsersFromAllowedOrganizations + // +kubebuilder:validation:Enum=OnlyUsersFromAllowedOrganizations;AllGitHubUsers + // +optional + Policy *GitHubAllowedAuthOrganizationsPolicy `json:"policy"` + + // Allowed, when specified, indicates that only users with membership in at least one of the listed + // GitHub organizations may log in. In addition, the group membership presented to Kubernetes will only include + // teams within the listed GitHub organizations. Additional login rules or group filtering can optionally be + // provided as policy expression on any Pinniped Supervisor FederationDomain that includes this IDP. + // + // The configured GitHub App or GitHub OAuth App must be allowed to see membership in the listed organizations, + // otherwise Pinniped will not be aware that the user belongs to the listed organization or any teams + // within that organization. + // + // If no organizations are listed, you must set organizations: AllGitHubUsers. + // + // +kubebuilder:validation:MaxItems=64 + // +listType=set + // +optional + Allowed []string `json:"allowed,omitempty"` +} + +// GitHubAllowAuthenticationSpec allows customization of who can authenticate using this IDP and how. +type GitHubAllowAuthenticationSpec struct { + // Organizations allows customization of which organizations can authenticate using this IDP. + // +kubebuilder:validation:XValidation:message="spec.allowAuthentication.organizations.policy must be 'OnlyUsersFromAllowedOrganizations' when spec.allowAuthentication.organizations.allowed has organizations listed",rule="!(has(self.allowed) && size(self.allowed) > 0 && self.policy == 'AllGitHubUsers')" + // +kubebuilder:validation:XValidation:message="spec.allowAuthentication.organizations.policy must be 'AllGitHubUsers' when spec.allowAuthentication.organizations.allowed is empty",rule="!((!has(self.allowed) || size(self.allowed) == 0) && self.policy == 'OnlyUsersFromAllowedOrganizations')" + Organizations GitHubOrganizationsSpec `json:"organizations"` +} + +// GitHubIdentityProviderSpec is the spec for configuring an GitHub identity provider. +type GitHubIdentityProviderSpec struct { + // GitHubAPI allows configuration for GitHub Enterprise Server + // + // +kubebuilder:default={} + GitHubAPI GitHubAPIConfig `json:"githubAPI,omitempty"` + + // Claims allows customization of the username and groups claims. + // + // +kubebuilder:default={} + Claims GitHubClaims `json:"claims,omitempty"` + + // AllowAuthentication allows customization of who can authenticate using this IDP and how. + AllowAuthentication GitHubAllowAuthenticationSpec `json:"allowAuthentication"` + + // Client identifies the secret with credentials for a GitHub App or GitHub OAuth2 App (a GitHub client). + Client GitHubClientSpec `json:"client"` +} + +// GitHubIdentityProvider describes the configuration of an upstream GitHub identity provider. +// This upstream provider can be configured with either a GitHub App or a GitHub OAuth2 App. +// +// Right now, only web-based logins are supported, for both the pinniped-cli client and clients configured +// as OIDCClients. +// +// +genclient +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object +// +kubebuilder:resource:categories=pinniped;pinniped-idp;pinniped-idps +// +kubebuilder:printcolumn:name="Host",type=string,JSONPath=`.spec.githubAPI.host` +// +kubebuilder:printcolumn:name="Status",type=string,JSONPath=`.status.phase` +// +kubebuilder:printcolumn:name="Age",type=date,JSONPath=`.metadata.creationTimestamp` +// +kubebuilder:subresource:status +type GitHubIdentityProvider struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + // Spec for configuring the identity provider. + Spec GitHubIdentityProviderSpec `json:"spec"` + + // Status of the identity provider. + Status GitHubIdentityProviderStatus `json:"status,omitempty"` +} + +// GitHubIdentityProviderList lists GitHubIdentityProvider objects. +// +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object +type GitHubIdentityProviderList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + + Items []GitHubIdentityProvider `json:"items"` +} diff --git a/apis/supervisor/idp/v1alpha1/types_tls.go.tmpl b/apis/supervisor/idp/v1alpha1/types_tls.go.tmpl index 1413a262c..49b49373c 100644 --- a/apis/supervisor/idp/v1alpha1/types_tls.go.tmpl +++ b/apis/supervisor/idp/v1alpha1/types_tls.go.tmpl @@ -1,9 +1,9 @@ -// Copyright 2020-2022 the Pinniped contributors. All Rights Reserved. +// Copyright 2020-2024 the Pinniped contributors. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 package v1alpha1 -// Configuration for TLS parameters related to identity provider integration. +// TLSSpec provides TLS configuration for identity provider integration. type TLSSpec struct { // X.509 Certificate Authority (base64-encoded PEM bundle). If omitted, a default set of system roots will be trusted. // +optional diff --git a/apis/supervisor/idpdiscovery/v1alpha1/types_supervisor_idp_discovery.go.tmpl b/apis/supervisor/idpdiscovery/v1alpha1/types_supervisor_idp_discovery.go.tmpl index 837fed2dd..11be6f9dc 100644 --- a/apis/supervisor/idpdiscovery/v1alpha1/types_supervisor_idp_discovery.go.tmpl +++ b/apis/supervisor/idpdiscovery/v1alpha1/types_supervisor_idp_discovery.go.tmpl @@ -15,6 +15,7 @@ const ( IDPTypeOIDC IDPType = "oidc" IDPTypeLDAP IDPType = "ldap" IDPTypeActiveDirectory IDPType = "activedirectory" + IDPTypeGitHub IDPType = "github" IDPFlowCLIPassword IDPFlow = "cli_password" IDPFlowBrowserAuthcode IDPFlow = "browser_authcode" diff --git a/cmd/pinniped/cmd/kubeconfig.go b/cmd/pinniped/cmd/kubeconfig.go index 193dde6fe..35c46f6a5 100644 --- a/cmd/pinniped/cmd/kubeconfig.go +++ b/cmd/pinniped/cmd/kubeconfig.go @@ -143,7 +143,18 @@ func kubeconfigCommand(deps kubeconfigDeps) *cobra.Command { f.BoolVar(&flags.oidc.debugSessionCache, "oidc-debug-session-cache", false, "Print debug logs related to the OpenID Connect session cache") f.StringVar(&flags.oidc.requestAudience, "oidc-request-audience", "", "Request a token with an alternate audience using RFC8693 token exchange") f.StringVar(&flags.oidc.upstreamIDPName, "upstream-identity-provider-name", "", "The name of the upstream identity provider used during login with a Supervisor") - f.StringVar(&flags.oidc.upstreamIDPType, "upstream-identity-provider-type", "", fmt.Sprintf("The type of the upstream identity provider used during login with a Supervisor (e.g. '%s', '%s', '%s')", idpdiscoveryv1alpha1.IDPTypeOIDC, idpdiscoveryv1alpha1.IDPTypeLDAP, idpdiscoveryv1alpha1.IDPTypeActiveDirectory)) + f.StringVar( + &flags.oidc.upstreamIDPType, + "upstream-identity-provider-type", + "", + fmt.Sprintf( + "The type of the upstream identity provider used during login with a Supervisor (e.g. '%s', '%s', '%s', '%s')", + idpdiscoveryv1alpha1.IDPTypeOIDC, + idpdiscoveryv1alpha1.IDPTypeLDAP, + idpdiscoveryv1alpha1.IDPTypeActiveDirectory, + idpdiscoveryv1alpha1.IDPTypeGitHub, + ), + ) f.StringVar(&flags.oidc.upstreamIDPFlow, "upstream-identity-provider-flow", "", fmt.Sprintf("The type of client flow to use with the upstream identity provider during login with a Supervisor (e.g. '%s', '%s')", idpdiscoveryv1alpha1.IDPFlowCLIPassword, idpdiscoveryv1alpha1.IDPFlowBrowserAuthcode)) f.StringVar(&flags.kubeconfigPath, "kubeconfig", os.Getenv("KUBECONFIG"), "Path to kubeconfig file") f.StringVar(&flags.kubeconfigContextOverride, "kubeconfig-context", "", "Kubeconfig context name (default: current active context)") diff --git a/cmd/pinniped/cmd/kubeconfig_test.go b/cmd/pinniped/cmd/kubeconfig_test.go index 0e6d74f59..237c34434 100644 --- a/cmd/pinniped/cmd/kubeconfig_test.go +++ b/cmd/pinniped/cmd/kubeconfig_test.go @@ -157,7 +157,7 @@ func TestGetKubeconfig(t *testing.T) { --timeout duration Timeout for autodiscovery and validation (default 10m0s) --upstream-identity-provider-flow string The type of client flow to use with the upstream identity provider during login with a Supervisor (e.g. 'cli_password', 'browser_authcode') --upstream-identity-provider-name string The name of the upstream identity provider used during login with a Supervisor - --upstream-identity-provider-type string The type of the upstream identity provider used during login with a Supervisor (e.g. 'oidc', 'ldap', 'activedirectory') + --upstream-identity-provider-type string The type of the upstream identity provider used during login with a Supervisor (e.g. 'oidc', 'ldap', 'activedirectory', 'github') `) }, }, @@ -909,7 +909,8 @@ func TestGetKubeconfig(t *testing.T) { idpsDiscoveryResponse: here.Docf(`{ "pinniped_identity_providers": [ {"name": "some-ldap-idp", "type": "ldap"}, - {"name": "some-oidc-idp", "type": "oidc", "flows": ["flow1", "flow2"]} + {"name": "some-oidc-idp", "type": "oidc", "flows": ["flow1", "flow2"]}, + {"name": "some-github-idp", "type": "github"} ] }`), wantLogs: func(issuerCABundle string, issuerURL string) []string { @@ -928,7 +929,7 @@ func TestGetKubeconfig(t *testing.T) { wantStderr: func(issuerCABundle string, issuerURL string) testutil.RequireErrorStringFunc { return testutil.WantExactErrorString(`Error: multiple Supervisor upstream identity providers were found, ` + `so the --upstream-identity-provider-name/--upstream-identity-provider-type flags must be specified. ` + - `Found these upstreams: [{"name":"some-ldap-idp","type":"ldap"},{"name":"some-oidc-idp","type":"oidc","flows":["flow1","flow2"]}]` + "\n") + `Found these upstreams: [{"name":"some-ldap-idp","type":"ldap"},{"name":"some-oidc-idp","type":"oidc","flows":["flow1","flow2"]},{"name":"some-github-idp","type":"github"}]` + "\n") }, }, { diff --git a/cmd/pinniped/cmd/login_oidc.go b/cmd/pinniped/cmd/login_oidc.go index d771b5178..057de7729 100644 --- a/cmd/pinniped/cmd/login_oidc.go +++ b/cmd/pinniped/cmd/login_oidc.go @@ -141,7 +141,16 @@ func oidcLoginCommand(deps oidcLoginCommandDeps) *cobra.Command { cmd.Flags().StringVar(&flags.conciergeAPIGroupSuffix, "concierge-api-group-suffix", groupsuffix.PinnipedDefaultSuffix, "Concierge API group suffix") cmd.Flags().StringVar(&flags.credentialCachePath, "credential-cache", filepath.Join(mustGetConfigDir(), "credentials.yaml"), "Path to cluster-specific credentials cache (\"\" disables the cache)") cmd.Flags().StringVar(&flags.upstreamIdentityProviderName, "upstream-identity-provider-name", "", "The name of the upstream identity provider used during login with a Supervisor") - cmd.Flags().StringVar(&flags.upstreamIdentityProviderType, "upstream-identity-provider-type", idpdiscoveryv1alpha1.IDPTypeOIDC.String(), fmt.Sprintf("The type of the upstream identity provider used during login with a Supervisor (e.g. '%s', '%s', '%s')", idpdiscoveryv1alpha1.IDPTypeOIDC, idpdiscoveryv1alpha1.IDPTypeLDAP, idpdiscoveryv1alpha1.IDPTypeActiveDirectory)) + cmd.Flags().StringVar(&flags.upstreamIdentityProviderType, + "upstream-identity-provider-type", + idpdiscoveryv1alpha1.IDPTypeOIDC.String(), + fmt.Sprintf( + "The type of the upstream identity provider used during login with a Supervisor (e.g. '%s', '%s', '%s', '%s')", + idpdiscoveryv1alpha1.IDPTypeOIDC, + idpdiscoveryv1alpha1.IDPTypeLDAP, + idpdiscoveryv1alpha1.IDPTypeActiveDirectory, + idpdiscoveryv1alpha1.IDPTypeGitHub, + )) cmd.Flags().StringVar(&flags.upstreamIdentityProviderFlow, "upstream-identity-provider-flow", "", fmt.Sprintf("The type of client flow to use with the upstream identity provider during login with a Supervisor (e.g. '%s', '%s')", idpdiscoveryv1alpha1.IDPFlowBrowserAuthcode, idpdiscoveryv1alpha1.IDPFlowCLIPassword)) // --skip-listen is mainly needed for testing. We'll leave it hidden until we have a non-testing use case. diff --git a/cmd/pinniped/cmd/login_oidc_test.go b/cmd/pinniped/cmd/login_oidc_test.go index 5bd6f1884..06c544b61 100644 --- a/cmd/pinniped/cmd/login_oidc_test.go +++ b/cmd/pinniped/cmd/login_oidc_test.go @@ -103,7 +103,7 @@ func TestLoginOIDCCommand(t *testing.T) { --skip-browser Skip opening the browser (just print the URL) --upstream-identity-provider-flow string The type of client flow to use with the upstream identity provider during login with a Supervisor (e.g. 'browser_authcode', 'cli_password') --upstream-identity-provider-name string The name of the upstream identity provider used during login with a Supervisor - --upstream-identity-provider-type string The type of the upstream identity provider used during login with a Supervisor (e.g. 'oidc', 'ldap', 'activedirectory') (default "oidc") + --upstream-identity-provider-type string The type of the upstream identity provider used during login with a Supervisor (e.g. 'oidc', 'ldap', 'activedirectory', 'github') (default "oidc") `), }, { @@ -274,8 +274,8 @@ func TestLoginOIDCCommand(t *testing.T) { wantOptionsCount: 4, wantStdout: `{"kind":"ExecCredential","apiVersion":"client.authentication.k8s.io/v1beta1","spec":{"interactive":false},"status":{"expirationTimestamp":"3020-10-12T13:14:15Z","token":"test-id-token"}}` + "\n", wantLogs: []string{ - nowStr + ` pinniped-login cmd/login_oidc.go:259 Performing OIDC login {"issuer": "test-issuer", "client id": "test-client-id"}`, - nowStr + ` pinniped-login cmd/login_oidc.go:279 No concierge configured, skipping token credential exchange`, + nowStr + ` pinniped-login cmd/login_oidc.go:268 Performing OIDC login {"issuer": "test-issuer", "client id": "test-client-id"}`, + nowStr + ` pinniped-login cmd/login_oidc.go:288 No concierge configured, skipping token credential exchange`, }, }, { @@ -319,10 +319,10 @@ func TestLoginOIDCCommand(t *testing.T) { wantOptionsCount: 12, wantStdout: `{"kind":"ExecCredential","apiVersion":"client.authentication.k8s.io/v1beta1","spec":{"interactive":false},"status":{"token":"exchanged-token"}}` + "\n", wantLogs: []string{ - nowStr + ` pinniped-login cmd/login_oidc.go:259 Performing OIDC login {"issuer": "test-issuer", "client id": "test-client-id"}`, - nowStr + ` pinniped-login cmd/login_oidc.go:269 Exchanging token for cluster credential {"endpoint": "https://127.0.0.1:1234/", "authenticator type": "webhook", "authenticator name": "test-authenticator"}`, - nowStr + ` pinniped-login cmd/login_oidc.go:277 Successfully exchanged token for cluster credential.`, - nowStr + ` pinniped-login cmd/login_oidc.go:284 caching cluster credential for future use.`, + nowStr + ` pinniped-login cmd/login_oidc.go:268 Performing OIDC login {"issuer": "test-issuer", "client id": "test-client-id"}`, + nowStr + ` pinniped-login cmd/login_oidc.go:278 Exchanging token for cluster credential {"endpoint": "https://127.0.0.1:1234/", "authenticator type": "webhook", "authenticator name": "test-authenticator"}`, + nowStr + ` pinniped-login cmd/login_oidc.go:286 Successfully exchanged token for cluster credential.`, + nowStr + ` pinniped-login cmd/login_oidc.go:293 caching cluster credential for future use.`, }, }, } diff --git a/deploy/concierge/authentication.concierge.pinniped.dev_jwtauthenticators.yaml b/deploy/concierge/authentication.concierge.pinniped.dev_jwtauthenticators.yaml index b464cfa2b..d59fcb783 100644 --- a/deploy/concierge/authentication.concierge.pinniped.dev_jwtauthenticators.yaml +++ b/deploy/concierge/authentication.concierge.pinniped.dev_jwtauthenticators.yaml @@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - controller-gen.kubebuilder.io/version: v0.14.0 + controller-gen.kubebuilder.io/version: v0.15.0 name: jwtauthenticators.authentication.concierge.pinniped.dev spec: group: authentication.concierge.pinniped.dev diff --git a/deploy/concierge/authentication.concierge.pinniped.dev_webhookauthenticators.yaml b/deploy/concierge/authentication.concierge.pinniped.dev_webhookauthenticators.yaml index e8cbc6790..4ccd53770 100644 --- a/deploy/concierge/authentication.concierge.pinniped.dev_webhookauthenticators.yaml +++ b/deploy/concierge/authentication.concierge.pinniped.dev_webhookauthenticators.yaml @@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - controller-gen.kubebuilder.io/version: v0.14.0 + controller-gen.kubebuilder.io/version: v0.15.0 name: webhookauthenticators.authentication.concierge.pinniped.dev spec: group: authentication.concierge.pinniped.dev diff --git a/deploy/concierge/config.concierge.pinniped.dev_credentialissuers.yaml b/deploy/concierge/config.concierge.pinniped.dev_credentialissuers.yaml index 8d77a6665..26fed7376 100644 --- a/deploy/concierge/config.concierge.pinniped.dev_credentialissuers.yaml +++ b/deploy/concierge/config.concierge.pinniped.dev_credentialissuers.yaml @@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - controller-gen.kubebuilder.io/version: v0.14.0 + controller-gen.kubebuilder.io/version: v0.15.0 name: credentialissuers.config.concierge.pinniped.dev spec: group: config.concierge.pinniped.dev diff --git a/deploy/supervisor/config.supervisor.pinniped.dev_federationdomains.yaml b/deploy/supervisor/config.supervisor.pinniped.dev_federationdomains.yaml index 7bf231443..033513431 100644 --- a/deploy/supervisor/config.supervisor.pinniped.dev_federationdomains.yaml +++ b/deploy/supervisor/config.supervisor.pinniped.dev_federationdomains.yaml @@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - controller-gen.kubebuilder.io/version: v0.14.0 + controller-gen.kubebuilder.io/version: v0.15.0 name: federationdomains.config.supervisor.pinniped.dev spec: group: config.supervisor.pinniped.dev @@ -421,10 +421,15 @@ spec: exist. properties: name: + default: "" description: |- Name of the referent. - More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. TODO: Add other useful fields. apiVersion, kind, uid? + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Drop `kubebuilder:default` when controller-gen doesn't need it https://github.com/kubernetes-sigs/kubebuilder/issues/3896. type: string type: object x-kubernetes-map-type: atomic @@ -434,10 +439,15 @@ spec: encrypting state parameters is stored. properties: name: + default: "" description: |- Name of the referent. - More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. TODO: Add other useful fields. apiVersion, kind, uid? + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Drop `kubebuilder:default` when controller-gen doesn't need it https://github.com/kubernetes-sigs/kubebuilder/issues/3896. type: string type: object x-kubernetes-map-type: atomic @@ -447,10 +457,15 @@ spec: signing state parameters is stored. properties: name: + default: "" description: |- Name of the referent. - More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. TODO: Add other useful fields. apiVersion, kind, uid? + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Drop `kubebuilder:default` when controller-gen doesn't need it https://github.com/kubernetes-sigs/kubebuilder/issues/3896. type: string type: object x-kubernetes-map-type: atomic @@ -460,10 +475,15 @@ spec: signing tokens is stored. properties: name: + default: "" description: |- Name of the referent. - More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. TODO: Add other useful fields. apiVersion, kind, uid? + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Drop `kubebuilder:default` when controller-gen doesn't need it https://github.com/kubernetes-sigs/kubebuilder/issues/3896. type: string type: object x-kubernetes-map-type: atomic diff --git a/deploy/supervisor/config.supervisor.pinniped.dev_oidcclients.yaml b/deploy/supervisor/config.supervisor.pinniped.dev_oidcclients.yaml index d33b31bf1..c44ecbf1e 100644 --- a/deploy/supervisor/config.supervisor.pinniped.dev_oidcclients.yaml +++ b/deploy/supervisor/config.supervisor.pinniped.dev_oidcclients.yaml @@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - controller-gen.kubebuilder.io/version: v0.14.0 + controller-gen.kubebuilder.io/version: v0.15.0 name: oidcclients.config.supervisor.pinniped.dev spec: group: config.supervisor.pinniped.dev diff --git a/deploy/supervisor/idp.supervisor.pinniped.dev_activedirectoryidentityproviders.yaml b/deploy/supervisor/idp.supervisor.pinniped.dev_activedirectoryidentityproviders.yaml index 414ac4f51..062251102 100644 --- a/deploy/supervisor/idp.supervisor.pinniped.dev_activedirectoryidentityproviders.yaml +++ b/deploy/supervisor/idp.supervisor.pinniped.dev_activedirectoryidentityproviders.yaml @@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - controller-gen.kubebuilder.io/version: v0.14.0 + controller-gen.kubebuilder.io/version: v0.15.0 name: activedirectoryidentityproviders.idp.supervisor.pinniped.dev spec: group: idp.supervisor.pinniped.dev diff --git a/deploy/supervisor/idp.supervisor.pinniped.dev_githubidentityproviders.yaml b/deploy/supervisor/idp.supervisor.pinniped.dev_githubidentityproviders.yaml new file mode 100644 index 000000000..f93108700 --- /dev/null +++ b/deploy/supervisor/idp.supervisor.pinniped.dev_githubidentityproviders.yaml @@ -0,0 +1,326 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.15.0 + name: githubidentityproviders.idp.supervisor.pinniped.dev +spec: + group: idp.supervisor.pinniped.dev + names: + categories: + - pinniped + - pinniped-idp + - pinniped-idps + kind: GitHubIdentityProvider + listKind: GitHubIdentityProviderList + plural: githubidentityproviders + singular: githubidentityprovider + scope: Namespaced + versions: + - additionalPrinterColumns: + - jsonPath: .spec.githubAPI.host + name: Host + type: string + - jsonPath: .status.phase + name: Status + type: string + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1alpha1 + schema: + openAPIV3Schema: + description: |- + GitHubIdentityProvider describes the configuration of an upstream GitHub identity provider. + This upstream provider can be configured with either a GitHub App or a GitHub OAuth2 App. + + + Right now, only web-based logins are supported, for both the pinniped-cli client and clients configured + as OIDCClients. + 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: + description: Spec for configuring the identity provider. + properties: + allowAuthentication: + description: AllowAuthentication allows customization of who can authenticate + using this IDP and how. + properties: + organizations: + description: Organizations allows customization of which organizations + can authenticate using this IDP. + properties: + allowed: + description: |- + Allowed, when specified, indicates that only users with membership in at least one of the listed + GitHub organizations may log in. In addition, the group membership presented to Kubernetes will only include + teams within the listed GitHub organizations. Additional login rules or group filtering can optionally be + provided as policy expression on any Pinniped Supervisor FederationDomain that includes this IDP. + + + The configured GitHub App or GitHub OAuth App must be allowed to see membership in the listed organizations, + otherwise Pinniped will not be aware that the user belongs to the listed organization or any teams + within that organization. + + + If no organizations are listed, you must set organizations: AllGitHubUsers. + items: + type: string + maxItems: 64 + type: array + x-kubernetes-list-type: set + policy: + default: OnlyUsersFromAllowedOrganizations + description: |- + Policy must be set to "AllGitHubUsers" if allowed is empty. + + + This field only exists to ensure that Pinniped administrators are aware that an empty list of + allowedOrganizations means all GitHub users are allowed to log in. + enum: + - OnlyUsersFromAllowedOrganizations + - AllGitHubUsers + type: string + type: object + x-kubernetes-validations: + - message: spec.allowAuthentication.organizations.policy must + be 'OnlyUsersFromAllowedOrganizations' when spec.allowAuthentication.organizations.allowed + has organizations listed + rule: '!(has(self.allowed) && size(self.allowed) > 0 && self.policy + == ''AllGitHubUsers'')' + - message: spec.allowAuthentication.organizations.policy must + be 'AllGitHubUsers' when spec.allowAuthentication.organizations.allowed + is empty + rule: '!((!has(self.allowed) || size(self.allowed) == 0) && + self.policy == ''OnlyUsersFromAllowedOrganizations'')' + required: + - organizations + type: object + claims: + default: {} + description: Claims allows customization of the username and groups + claims. + properties: + groups: + default: slug + description: |- + Groups configures which property of the GitHub team record shall determine the group names in Kubernetes. + + + Can be either "name" or "slug". Defaults to "slug". + + + GitHub team names can contain upper and lower case characters, whitespace, and punctuation (e.g. "Kube admins!"). + + + GitHub team slugs are lower case alphanumeric characters and may contain dashes and underscores (e.g. "kube-admins"). + + + Group names as presented to Kubernetes will always be prefixed by the GitHub organization name followed by a + forward slash (e.g. "my-org/my-team"). GitHub organization login names can only contain alphanumeric characters + or single hyphens, so the first forward slash `/` will be the separator between the organization login name and + the team name or slug. + + + If desired, an admin could configure identity transformation expressions on the Pinniped Supervisor's + FederationDomain to further customize how these group names are presented to Kubernetes. + + + See the response schema for + [List teams for the authenticated user](https://docs.github.com/en/rest/teams/teams?apiVersion=2022-11-28#list-teams-for-the-authenticated-user). + enum: + - name + - slug + type: string + username: + default: login:id + description: |- + Username configures which property of the GitHub user record shall determine the username in Kubernetes. + + + Can be either "id", "login", or "login:id". Defaults to "login:id". + + + GitHub's user login attributes can only contain alphanumeric characters and non-repeating hyphens, + and may not start or end with hyphens. GitHub users are allowed to change their login name, + although it is inconvenient. If a GitHub user changed their login name from "foo" to "bar", + then a second user might change their name from "baz" to "foo" in order to take the old + username of the first user. For this reason, it is not as safe to make authorization decisions + based only on the user's login attribute. + + + If desired, an admin could configure identity transformation expressions on the Pinniped Supervisor's + FederationDomain to further customize how these usernames are presented to Kubernetes. + + + Defaults to "login:id", which is the user login attribute, followed by a colon, followed by the unique and + unchanging integer ID number attribute. This blends human-readable login names with the unchanging ID value + from GitHub. Colons are not allowed in GitHub login attributes or ID numbers, so this is a reasonable + choice to concatenate the two values. + + + See the response schema for + [Get the authenticated user](https://docs.github.com/en/rest/users/users?apiVersion=2022-11-28#get-the-authenticated-user). + enum: + - id + - login + - login:id + type: string + type: object + client: + description: Client identifies the secret with credentials for a GitHub + App or GitHub OAuth2 App (a GitHub client). + properties: + secretName: + description: |- + SecretName contains the name of a namespace-local Secret object that provides the clientID and + clientSecret for an GitHub App or GitHub OAuth2 client. + + + This secret must be of type "secrets.pinniped.dev/github-client" with keys "clientID" and "clientSecret". + minLength: 1 + type: string + required: + - secretName + type: object + githubAPI: + default: {} + description: GitHubAPI allows configuration for GitHub Enterprise + Server + properties: + host: + default: github.com + description: |- + Host is required only for GitHub Enterprise Server. + Defaults to using GitHub's public API ("github.com"). + Do not specify a protocol or scheme since "https://" will always be used. + Port is optional. Do not specify a path, query, fragment, or userinfo. + Only domain name or IP address, subdomains (optional), and port (optional). + IPv4 and IPv6 are supported. If using an IPv6 address with a port, you must enclose the IPv6 address + in square brackets. Example: "[::1]:443". + minLength: 1 + type: string + tls: + description: TLS configuration for GitHub Enterprise Server. + properties: + certificateAuthorityData: + description: X.509 Certificate Authority (base64-encoded PEM + bundle). If omitted, a default set of system roots will + be trusted. + type: string + type: object + type: object + required: + - allowAuthentication + - client + type: object + status: + description: Status of the identity provider. + properties: + conditions: + description: Conditions represents the observations of an identity + provider's current state. + items: + description: "Condition contains details for one aspect of the current + state of this API Resource.\n---\nThis struct is intended for + direct use as an array at the field path .status.conditions. For + example,\n\n\n\ttype FooStatus struct{\n\t // Represents the + observations of a foo's current state.\n\t // Known .status.conditions.type + are: \"Available\", \"Progressing\", and \"Degraded\"\n\t // + +patchMergeKey=type\n\t // +patchStrategy=merge\n\t // +listType=map\n\t + \ // +listMapKey=type\n\t Conditions []metav1.Condition `json:\"conditions,omitempty\" + patchStrategy:\"merge\" patchMergeKey:\"type\" protobuf:\"bytes,1,rep,name=conditions\"`\n\n\n\t + \ // other fields\n\t}" + properties: + lastTransitionTime: + description: |- + lastTransitionTime is the last time the condition transitioned from one status to another. + This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: |- + message is a human readable message indicating details about the transition. + This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: |- + observedGeneration represents the .metadata.generation that the condition was set based upon. + For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date + with respect to the current state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: |- + reason contains a programmatic identifier indicating the reason for the condition's last transition. + Producers of specific condition types may define expected values and meanings for this field, + and whether the values are considered a guaranteed API. + The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: |- + type of condition in CamelCase or in foo.example.com/CamelCase. + --- + Many .condition.type values are consistent across resources like Available, but because arbitrary conditions can be + useful (see .node.status.conditions), the ability to deconflict is important. + The regex it matches is (dns1123SubdomainFmt/)?(qualifiedNameFmt) + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + x-kubernetes-list-map-keys: + - type + x-kubernetes-list-type: map + phase: + default: Pending + description: Phase summarizes the overall status of the GitHubIdentityProvider. + enum: + - Pending + - Ready + - Error + type: string + type: object + required: + - spec + type: object + served: true + storage: true + subresources: + status: {} diff --git a/deploy/supervisor/idp.supervisor.pinniped.dev_ldapidentityproviders.yaml b/deploy/supervisor/idp.supervisor.pinniped.dev_ldapidentityproviders.yaml index f718e93aa..711e9a754 100644 --- a/deploy/supervisor/idp.supervisor.pinniped.dev_ldapidentityproviders.yaml +++ b/deploy/supervisor/idp.supervisor.pinniped.dev_ldapidentityproviders.yaml @@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - controller-gen.kubebuilder.io/version: v0.14.0 + controller-gen.kubebuilder.io/version: v0.15.0 name: ldapidentityproviders.idp.supervisor.pinniped.dev spec: group: idp.supervisor.pinniped.dev diff --git a/deploy/supervisor/idp.supervisor.pinniped.dev_oidcidentityproviders.yaml b/deploy/supervisor/idp.supervisor.pinniped.dev_oidcidentityproviders.yaml index 486665605..acfca1573 100644 --- a/deploy/supervisor/idp.supervisor.pinniped.dev_oidcidentityproviders.yaml +++ b/deploy/supervisor/idp.supervisor.pinniped.dev_oidcidentityproviders.yaml @@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - controller-gen.kubebuilder.io/version: v0.14.0 + controller-gen.kubebuilder.io/version: v0.15.0 name: oidcidentityproviders.idp.supervisor.pinniped.dev spec: group: idp.supervisor.pinniped.dev diff --git a/deploy/supervisor/rbac.yaml b/deploy/supervisor/rbac.yaml index cc5714c11..2d51e383a 100644 --- a/deploy/supervisor/rbac.yaml +++ b/deploy/supervisor/rbac.yaml @@ -56,6 +56,14 @@ rules: - #@ pinnipedDevAPIGroupWithPrefix("idp.supervisor") resources: [activedirectoryidentityproviders/status] verbs: [get, patch, update] + - apiGroups: + - #@ pinnipedDevAPIGroupWithPrefix("idp.supervisor") + resources: [githubidentityproviders] + verbs: [get, list, watch] + - apiGroups: + - #@ pinnipedDevAPIGroupWithPrefix("idp.supervisor") + resources: [githubidentityproviders/status] + verbs: [get, patch, update] #! We want to be able to read pods/replicasets/deployment so we can learn who our deployment is to set #! as an owner reference. - apiGroups: [""] diff --git a/deploy/supervisor/z0_crd_overlay.yaml b/deploy/supervisor/z0_crd_overlay.yaml index f7a50a88d..889c0166b 100644 --- a/deploy/supervisor/z0_crd_overlay.yaml +++ b/deploy/supervisor/z0_crd_overlay.yaml @@ -1,4 +1,4 @@ -#! Copyright 2020-2022 the Pinniped contributors. All Rights Reserved. +#! Copyright 2020-2024 the Pinniped contributors. All Rights Reserved. #! SPDX-License-Identifier: Apache-2.0 #@ load("@ytt:overlay", "overlay") @@ -41,6 +41,15 @@ metadata: spec: group: #@ pinnipedDevAPIGroupWithPrefix("idp.supervisor") +#@overlay/match by=overlay.subset({"kind": "CustomResourceDefinition", "metadata":{"name":"githubidentityproviders.idp.supervisor.pinniped.dev"}}), expects=1 +--- +metadata: + #@overlay/match missing_ok=True + labels: #@ labels() + name: #@ pinnipedDevAPIGroupWithPrefix("githubidentityproviders.idp.supervisor") +spec: + group: #@ pinnipedDevAPIGroupWithPrefix("idp.supervisor") + #@overlay/match by=overlay.subset({"kind": "CustomResourceDefinition", "metadata":{"name":"oidcclients.config.supervisor.pinniped.dev"}}), expects=1 --- metadata: diff --git a/generated/1.24/README.adoc b/generated/1.24/README.adoc index c198b6584..3311e31fc 100644 --- a/generated/1.24/README.adoc +++ b/generated/1.24/README.adoc @@ -1645,6 +1645,285 @@ Optional, when empty this defaults to "objectGUID". + |=== +[id="{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-supervisor-idp-v1alpha1-githubapiconfig"] +==== GitHubAPIConfig + +GitHubAPIConfig allows configuration for GitHub Enterprise Server + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-supervisor-idp-v1alpha1-githubidentityproviderspec[$$GitHubIdentityProviderSpec$$] +**** + +[cols="25a,75a", options="header"] +|=== +| Field | Description +| *`host`* __string__ | Host is required only for GitHub Enterprise Server. + +Defaults to using GitHub's public API ("github.com"). + +Do not specify a protocol or scheme since "https://" will always be used. + +Port is optional. Do not specify a path, query, fragment, or userinfo. + +Only domain name or IP address, subdomains (optional), and port (optional). + +IPv4 and IPv6 are supported. If using an IPv6 address with a port, you must enclose the IPv6 address + +in square brackets. Example: "[::1]:443". + +| *`tls`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-supervisor-idp-v1alpha1-tlsspec[$$TLSSpec$$]__ | TLS configuration for GitHub Enterprise Server. + +|=== + + +[id="{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-supervisor-idp-v1alpha1-githuballowauthenticationspec"] +==== GitHubAllowAuthenticationSpec + +GitHubAllowAuthenticationSpec allows customization of who can authenticate using this IDP and how. + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-supervisor-idp-v1alpha1-githubidentityproviderspec[$$GitHubIdentityProviderSpec$$] +**** + +[cols="25a,75a", options="header"] +|=== +| Field | Description +| *`organizations`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-supervisor-idp-v1alpha1-githuborganizationsspec[$$GitHubOrganizationsSpec$$]__ | Organizations allows customization of which organizations can authenticate using this IDP. + +|=== + + +[id="{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-supervisor-idp-v1alpha1-githuballowedauthorganizationspolicy"] +==== GitHubAllowedAuthOrganizationsPolicy (string) + + + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-supervisor-idp-v1alpha1-githuborganizationsspec[$$GitHubOrganizationsSpec$$] +**** + + + +[id="{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-supervisor-idp-v1alpha1-githubclaims"] +==== GitHubClaims + +GitHubClaims allows customization of the username and groups claims. + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-supervisor-idp-v1alpha1-githubidentityproviderspec[$$GitHubIdentityProviderSpec$$] +**** + +[cols="25a,75a", options="header"] +|=== +| Field | Description +| *`username`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-supervisor-idp-v1alpha1-githubusernameattribute[$$GitHubUsernameAttribute$$]__ | Username configures which property of the GitHub user record shall determine the username in Kubernetes. + + + +Can be either "id", "login", or "login:id". Defaults to "login:id". + + + +GitHub's user login attributes can only contain alphanumeric characters and non-repeating hyphens, + +and may not start or end with hyphens. GitHub users are allowed to change their login name, + +although it is inconvenient. If a GitHub user changed their login name from "foo" to "bar", + +then a second user might change their name from "baz" to "foo" in order to take the old + +username of the first user. For this reason, it is not as safe to make authorization decisions + +based only on the user's login attribute. + + + +If desired, an admin could configure identity transformation expressions on the Pinniped Supervisor's + +FederationDomain to further customize how these usernames are presented to Kubernetes. + + + +Defaults to "login:id", which is the user login attribute, followed by a colon, followed by the unique and + +unchanging integer ID number attribute. This blends human-readable login names with the unchanging ID value + +from GitHub. Colons are not allowed in GitHub login attributes or ID numbers, so this is a reasonable + +choice to concatenate the two values. + + + +See the response schema for + +[Get the authenticated user](https://docs.github.com/en/rest/users/users?apiVersion=2022-11-28#get-the-authenticated-user). + +| *`groups`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-supervisor-idp-v1alpha1-githubgroupnameattribute[$$GitHubGroupNameAttribute$$]__ | Groups configures which property of the GitHub team record shall determine the group names in Kubernetes. + + + +Can be either "name" or "slug". Defaults to "slug". + + + +GitHub team names can contain upper and lower case characters, whitespace, and punctuation (e.g. "Kube admins!"). + + + +GitHub team slugs are lower case alphanumeric characters and may contain dashes and underscores (e.g. "kube-admins"). + + + +Group names as presented to Kubernetes will always be prefixed by the GitHub organization name followed by a + +forward slash (e.g. "my-org/my-team"). GitHub organization login names can only contain alphanumeric characters + +or single hyphens, so the first forward slash `/` will be the separator between the organization login name and + +the team name or slug. + + + +If desired, an admin could configure identity transformation expressions on the Pinniped Supervisor's + +FederationDomain to further customize how these group names are presented to Kubernetes. + + + +See the response schema for + +[List teams for the authenticated user](https://docs.github.com/en/rest/teams/teams?apiVersion=2022-11-28#list-teams-for-the-authenticated-user). + +|=== + + +[id="{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-supervisor-idp-v1alpha1-githubclientspec"] +==== GitHubClientSpec + +GitHubClientSpec contains information about the GitHub client that this identity provider will use +for web-based login flows. + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-supervisor-idp-v1alpha1-githubidentityproviderspec[$$GitHubIdentityProviderSpec$$] +**** + +[cols="25a,75a", options="header"] +|=== +| Field | Description +| *`secretName`* __string__ | SecretName contains the name of a namespace-local Secret object that provides the clientID and + +clientSecret for an GitHub App or GitHub OAuth2 client. + + + +This secret must be of type "secrets.pinniped.dev/github-client" with keys "clientID" and "clientSecret". + +|=== + + +[id="{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-supervisor-idp-v1alpha1-githubgroupnameattribute"] +==== GitHubGroupNameAttribute (string) + +GitHubGroupNameAttribute allows the user to specify which attribute from GitHub to use for the group +names to present to Kubernetes. See the response schema for +[List teams for the authenticated user](https://docs.github.com/en/rest/teams/teams?apiVersion=2022-11-28#list-teams-for-the-authenticated-user). + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-supervisor-idp-v1alpha1-githubclaims[$$GitHubClaims$$] +**** + + + +[id="{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-supervisor-idp-v1alpha1-githubidentityprovider"] +==== GitHubIdentityProvider + +GitHubIdentityProvider describes the configuration of an upstream GitHub identity provider. +This upstream provider can be configured with either a GitHub App or a GitHub OAuth2 App. + + +Right now, only web-based logins are supported, for both the pinniped-cli client and clients configured +as OIDCClients. + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-supervisor-idp-v1alpha1-githubidentityproviderlist[$$GitHubIdentityProviderList$$] +**** + +[cols="25a,75a", options="header"] +|=== +| Field | Description +| *`metadata`* __link:https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.24/#objectmeta-v1-meta[$$ObjectMeta$$]__ | Refer to Kubernetes API documentation for fields of `metadata`. + +| *`spec`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-supervisor-idp-v1alpha1-githubidentityproviderspec[$$GitHubIdentityProviderSpec$$]__ | Spec for configuring the identity provider. + +| *`status`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-supervisor-idp-v1alpha1-githubidentityproviderstatus[$$GitHubIdentityProviderStatus$$]__ | Status of the identity provider. + +|=== + + + + +[id="{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-supervisor-idp-v1alpha1-githubidentityproviderphase"] +==== GitHubIdentityProviderPhase (string) + + + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-supervisor-idp-v1alpha1-githubidentityproviderstatus[$$GitHubIdentityProviderStatus$$] +**** + + + +[id="{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-supervisor-idp-v1alpha1-githubidentityproviderspec"] +==== GitHubIdentityProviderSpec + +GitHubIdentityProviderSpec is the spec for configuring an GitHub identity provider. + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-supervisor-idp-v1alpha1-githubidentityprovider[$$GitHubIdentityProvider$$] +**** + +[cols="25a,75a", options="header"] +|=== +| Field | Description +| *`githubAPI`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-supervisor-idp-v1alpha1-githubapiconfig[$$GitHubAPIConfig$$]__ | GitHubAPI allows configuration for GitHub Enterprise Server + +| *`claims`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-supervisor-idp-v1alpha1-githubclaims[$$GitHubClaims$$]__ | Claims allows customization of the username and groups claims. + +| *`allowAuthentication`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-supervisor-idp-v1alpha1-githuballowauthenticationspec[$$GitHubAllowAuthenticationSpec$$]__ | AllowAuthentication allows customization of who can authenticate using this IDP and how. + +| *`client`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-supervisor-idp-v1alpha1-githubclientspec[$$GitHubClientSpec$$]__ | Client identifies the secret with credentials for a GitHub App or GitHub OAuth2 App (a GitHub client). + +|=== + + +[id="{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-supervisor-idp-v1alpha1-githubidentityproviderstatus"] +==== GitHubIdentityProviderStatus + +GitHubIdentityProviderStatus is the status of an GitHub identity provider. + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-supervisor-idp-v1alpha1-githubidentityprovider[$$GitHubIdentityProvider$$] +**** + +[cols="25a,75a", options="header"] +|=== +| Field | Description +| *`phase`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-supervisor-idp-v1alpha1-githubidentityproviderphase[$$GitHubIdentityProviderPhase$$]__ | Phase summarizes the overall status of the GitHubIdentityProvider. + +| *`conditions`* __link:https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.24/#condition-v1-meta[$$Condition$$] array__ | Conditions represents the observations of an identity provider's current state. + +|=== + + +[id="{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-supervisor-idp-v1alpha1-githuborganizationsspec"] +==== GitHubOrganizationsSpec + + + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-supervisor-idp-v1alpha1-githuballowauthenticationspec[$$GitHubAllowAuthenticationSpec$$] +**** + +[cols="25a,75a", options="header"] +|=== +| Field | Description +| *`policy`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-supervisor-idp-v1alpha1-githuballowedauthorganizationspolicy[$$GitHubAllowedAuthOrganizationsPolicy$$]__ | Policy must be set to "AllGitHubUsers" if allowed is empty. + + + +This field only exists to ensure that Pinniped administrators are aware that an empty list of + +allowedOrganizations means all GitHub users are allowed to log in. + +| *`allowed`* __string array__ | Allowed, when specified, indicates that only users with membership in at least one of the listed + +GitHub organizations may log in. In addition, the group membership presented to Kubernetes will only include + +teams within the listed GitHub organizations. Additional login rules or group filtering can optionally be + +provided as policy expression on any Pinniped Supervisor FederationDomain that includes this IDP. + + + +The configured GitHub App or GitHub OAuth App must be allowed to see membership in the listed organizations, + +otherwise Pinniped will not be aware that the user belongs to the listed organization or any teams + +within that organization. + + + +If no organizations are listed, you must set organizations: AllGitHubUsers. + +|=== + + +[id="{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-supervisor-idp-v1alpha1-githubusernameattribute"] +==== GitHubUsernameAttribute (string) + +GitHubUsernameAttribute allows the user to specify which attribute(s) from GitHub to use for the username to present +to Kubernetes. See the response schema for +[Get the authenticated user](https://docs.github.com/en/rest/users/users?apiVersion=2022-11-28#get-the-authenticated-user). + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-supervisor-idp-v1alpha1-githubclaims[$$GitHubClaims$$] +**** + + + [id="{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-supervisor-idp-v1alpha1-ldapidentityprovider"] ==== LDAPIdentityProvider @@ -2108,11 +2387,12 @@ Parameter is a key/value pair which represents a parameter in an HTTP request. [id="{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-supervisor-idp-v1alpha1-tlsspec"] ==== TLSSpec -Configuration for TLS parameters related to identity provider integration. +TLSSpec provides TLS configuration for identity provider integration. .Appears In: **** - xref:{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-supervisor-idp-v1alpha1-activedirectoryidentityproviderspec[$$ActiveDirectoryIdentityProviderSpec$$] +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-supervisor-idp-v1alpha1-githubapiconfig[$$GitHubAPIConfig$$] - xref:{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-supervisor-idp-v1alpha1-ldapidentityproviderspec[$$LDAPIdentityProviderSpec$$] - xref:{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-supervisor-idp-v1alpha1-oidcidentityproviderspec[$$OIDCIdentityProviderSpec$$] **** diff --git a/generated/1.24/apis/supervisor/idp/v1alpha1/register.go b/generated/1.24/apis/supervisor/idp/v1alpha1/register.go index 8829a8638..705be8076 100644 --- a/generated/1.24/apis/supervisor/idp/v1alpha1/register.go +++ b/generated/1.24/apis/supervisor/idp/v1alpha1/register.go @@ -1,4 +1,4 @@ -// Copyright 2020-2022 the Pinniped contributors. All Rights Reserved. +// Copyright 2020-2024 the Pinniped contributors. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 package v1alpha1 @@ -36,6 +36,8 @@ func addKnownTypes(scheme *runtime.Scheme) error { &LDAPIdentityProviderList{}, &ActiveDirectoryIdentityProvider{}, &ActiveDirectoryIdentityProviderList{}, + &GitHubIdentityProvider{}, + &GitHubIdentityProviderList{}, ) metav1.AddToGroupVersion(scheme, SchemeGroupVersion) return nil diff --git a/generated/1.24/apis/supervisor/idp/v1alpha1/types_githubidentityprovider.go b/generated/1.24/apis/supervisor/idp/v1alpha1/types_githubidentityprovider.go new file mode 100644 index 000000000..c84f46dbd --- /dev/null +++ b/generated/1.24/apis/supervisor/idp/v1alpha1/types_githubidentityprovider.go @@ -0,0 +1,256 @@ +// Copyright 2024 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package v1alpha1 + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +type GitHubIdentityProviderPhase string + +const ( + // GitHubPhasePending is the default phase for newly-created GitHubIdentityProvider resources. + GitHubPhasePending GitHubIdentityProviderPhase = "Pending" + + // GitHubPhaseReady is the phase for an GitHubIdentityProvider resource in a healthy state. + GitHubPhaseReady GitHubIdentityProviderPhase = "Ready" + + // GitHubPhaseError is the phase for an GitHubIdentityProvider in an unhealthy state. + GitHubPhaseError GitHubIdentityProviderPhase = "Error" +) + +type GitHubAllowedAuthOrganizationsPolicy string + +const ( + // GitHubAllowedAuthOrganizationsPolicyAllGitHubUsers means any GitHub user is allowed to log in using this identity + // provider, regardless of their organization membership or lack thereof. + GitHubAllowedAuthOrganizationsPolicyAllGitHubUsers GitHubAllowedAuthOrganizationsPolicy = "AllGitHubUsers" + + // GitHubAllowedAuthOrganizationsPolicyOnlyUsersFromAllowedOrganizations means only those users with membership in + // the listed GitHub organizations are allowed to log in. + GitHubAllowedAuthOrganizationsPolicyOnlyUsersFromAllowedOrganizations GitHubAllowedAuthOrganizationsPolicy = "OnlyUsersFromAllowedOrganizations" +) + +// GitHubIdentityProviderStatus is the status of an GitHub identity provider. +type GitHubIdentityProviderStatus struct { + // Phase summarizes the overall status of the GitHubIdentityProvider. + // + // +kubebuilder:default=Pending + // +kubebuilder:validation:Enum=Pending;Ready;Error + Phase GitHubIdentityProviderPhase `json:"phase,omitempty"` + + // Conditions represents the observations of an identity provider's current state. + // + // +patchMergeKey=type + // +patchStrategy=merge + // +listType=map + // +listMapKey=type + Conditions []metav1.Condition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type"` +} + +// GitHubAPIConfig allows configuration for GitHub Enterprise Server +type GitHubAPIConfig struct { + // Host is required only for GitHub Enterprise Server. + // Defaults to using GitHub's public API ("github.com"). + // Do not specify a protocol or scheme since "https://" will always be used. + // Port is optional. Do not specify a path, query, fragment, or userinfo. + // Only domain name or IP address, subdomains (optional), and port (optional). + // IPv4 and IPv6 are supported. If using an IPv6 address with a port, you must enclose the IPv6 address + // in square brackets. Example: "[::1]:443". + // + // +kubebuilder:default="github.com" + // +kubebuilder:validation:MinLength=1 + // +optional + Host *string `json:"host"` + + // TLS configuration for GitHub Enterprise Server. + // + // +optional + TLS *TLSSpec `json:"tls,omitempty"` +} + +// GitHubUsernameAttribute allows the user to specify which attribute(s) from GitHub to use for the username to present +// to Kubernetes. See the response schema for +// [Get the authenticated user](https://docs.github.com/en/rest/users/users?apiVersion=2022-11-28#get-the-authenticated-user). +type GitHubUsernameAttribute string + +const ( + // GitHubUsernameID specifies using the `id` attribute from the GitHub user for the username to present to Kubernetes. + GitHubUsernameID GitHubUsernameAttribute = "id" + + // GitHubUsernameLogin specifies using the `login` attribute from the GitHub user as the username to present to Kubernetes. + GitHubUsernameLogin GitHubUsernameAttribute = "login" + + // GitHubUsernameLoginAndID specifies combining the `login` and `id` attributes from the GitHub user as the + // username to present to Kubernetes, separated by a colon. Example: "my-login:1234" + GitHubUsernameLoginAndID GitHubUsernameAttribute = "login:id" +) + +// GitHubGroupNameAttribute allows the user to specify which attribute from GitHub to use for the group +// names to present to Kubernetes. See the response schema for +// [List teams for the authenticated user](https://docs.github.com/en/rest/teams/teams?apiVersion=2022-11-28#list-teams-for-the-authenticated-user). +type GitHubGroupNameAttribute string + +const ( + // GitHubUseTeamNameForGroupName specifies using the GitHub team's `name` attribute as the group name to present to Kubernetes. + GitHubUseTeamNameForGroupName GitHubGroupNameAttribute = "name" + + // GitHubUseTeamSlugForGroupName specifies using the GitHub team's `slug` attribute as the group name to present to Kubernetes. + GitHubUseTeamSlugForGroupName GitHubGroupNameAttribute = "slug" +) + +// GitHubClaims allows customization of the username and groups claims. +type GitHubClaims struct { + // Username configures which property of the GitHub user record shall determine the username in Kubernetes. + // + // Can be either "id", "login", or "login:id". Defaults to "login:id". + // + // GitHub's user login attributes can only contain alphanumeric characters and non-repeating hyphens, + // and may not start or end with hyphens. GitHub users are allowed to change their login name, + // although it is inconvenient. If a GitHub user changed their login name from "foo" to "bar", + // then a second user might change their name from "baz" to "foo" in order to take the old + // username of the first user. For this reason, it is not as safe to make authorization decisions + // based only on the user's login attribute. + // + // If desired, an admin could configure identity transformation expressions on the Pinniped Supervisor's + // FederationDomain to further customize how these usernames are presented to Kubernetes. + // + // Defaults to "login:id", which is the user login attribute, followed by a colon, followed by the unique and + // unchanging integer ID number attribute. This blends human-readable login names with the unchanging ID value + // from GitHub. Colons are not allowed in GitHub login attributes or ID numbers, so this is a reasonable + // choice to concatenate the two values. + // + // See the response schema for + // [Get the authenticated user](https://docs.github.com/en/rest/users/users?apiVersion=2022-11-28#get-the-authenticated-user). + // + // +kubebuilder:default="login:id" + // +kubebuilder:validation:Enum={"id","login","login:id"} + // +optional + Username *GitHubUsernameAttribute `json:"username"` + + // Groups configures which property of the GitHub team record shall determine the group names in Kubernetes. + // + // Can be either "name" or "slug". Defaults to "slug". + // + // GitHub team names can contain upper and lower case characters, whitespace, and punctuation (e.g. "Kube admins!"). + // + // GitHub team slugs are lower case alphanumeric characters and may contain dashes and underscores (e.g. "kube-admins"). + // + // Group names as presented to Kubernetes will always be prefixed by the GitHub organization name followed by a + // forward slash (e.g. "my-org/my-team"). GitHub organization login names can only contain alphanumeric characters + // or single hyphens, so the first forward slash `/` will be the separator between the organization login name and + // the team name or slug. + // + // If desired, an admin could configure identity transformation expressions on the Pinniped Supervisor's + // FederationDomain to further customize how these group names are presented to Kubernetes. + // + // See the response schema for + // [List teams for the authenticated user](https://docs.github.com/en/rest/teams/teams?apiVersion=2022-11-28#list-teams-for-the-authenticated-user). + // + // +kubebuilder:default=slug + // +kubebuilder:validation:Enum=name;slug + // +optional + Groups *GitHubGroupNameAttribute `json:"groups"` +} + +// GitHubClientSpec contains information about the GitHub client that this identity provider will use +// for web-based login flows. +type GitHubClientSpec struct { + // SecretName contains the name of a namespace-local Secret object that provides the clientID and + // clientSecret for an GitHub App or GitHub OAuth2 client. + // + // This secret must be of type "secrets.pinniped.dev/github-client" with keys "clientID" and "clientSecret". + // + // +kubebuilder:validation:MinLength=1 + SecretName string `json:"secretName"` +} + +type GitHubOrganizationsSpec struct { + // Policy must be set to "AllGitHubUsers" if allowed is empty. + // + // This field only exists to ensure that Pinniped administrators are aware that an empty list of + // allowedOrganizations means all GitHub users are allowed to log in. + // + // +kubebuilder:default=OnlyUsersFromAllowedOrganizations + // +kubebuilder:validation:Enum=OnlyUsersFromAllowedOrganizations;AllGitHubUsers + // +optional + Policy *GitHubAllowedAuthOrganizationsPolicy `json:"policy"` + + // Allowed, when specified, indicates that only users with membership in at least one of the listed + // GitHub organizations may log in. In addition, the group membership presented to Kubernetes will only include + // teams within the listed GitHub organizations. Additional login rules or group filtering can optionally be + // provided as policy expression on any Pinniped Supervisor FederationDomain that includes this IDP. + // + // The configured GitHub App or GitHub OAuth App must be allowed to see membership in the listed organizations, + // otherwise Pinniped will not be aware that the user belongs to the listed organization or any teams + // within that organization. + // + // If no organizations are listed, you must set organizations: AllGitHubUsers. + // + // +kubebuilder:validation:MaxItems=64 + // +listType=set + // +optional + Allowed []string `json:"allowed,omitempty"` +} + +// GitHubAllowAuthenticationSpec allows customization of who can authenticate using this IDP and how. +type GitHubAllowAuthenticationSpec struct { + // Organizations allows customization of which organizations can authenticate using this IDP. + // +kubebuilder:validation:XValidation:message="spec.allowAuthentication.organizations.policy must be 'OnlyUsersFromAllowedOrganizations' when spec.allowAuthentication.organizations.allowed has organizations listed",rule="!(has(self.allowed) && size(self.allowed) > 0 && self.policy == 'AllGitHubUsers')" + // +kubebuilder:validation:XValidation:message="spec.allowAuthentication.organizations.policy must be 'AllGitHubUsers' when spec.allowAuthentication.organizations.allowed is empty",rule="!((!has(self.allowed) || size(self.allowed) == 0) && self.policy == 'OnlyUsersFromAllowedOrganizations')" + Organizations GitHubOrganizationsSpec `json:"organizations"` +} + +// GitHubIdentityProviderSpec is the spec for configuring an GitHub identity provider. +type GitHubIdentityProviderSpec struct { + // GitHubAPI allows configuration for GitHub Enterprise Server + // + // +kubebuilder:default={} + GitHubAPI GitHubAPIConfig `json:"githubAPI,omitempty"` + + // Claims allows customization of the username and groups claims. + // + // +kubebuilder:default={} + Claims GitHubClaims `json:"claims,omitempty"` + + // AllowAuthentication allows customization of who can authenticate using this IDP and how. + AllowAuthentication GitHubAllowAuthenticationSpec `json:"allowAuthentication"` + + // Client identifies the secret with credentials for a GitHub App or GitHub OAuth2 App (a GitHub client). + Client GitHubClientSpec `json:"client"` +} + +// GitHubIdentityProvider describes the configuration of an upstream GitHub identity provider. +// This upstream provider can be configured with either a GitHub App or a GitHub OAuth2 App. +// +// Right now, only web-based logins are supported, for both the pinniped-cli client and clients configured +// as OIDCClients. +// +// +genclient +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object +// +kubebuilder:resource:categories=pinniped;pinniped-idp;pinniped-idps +// +kubebuilder:printcolumn:name="Host",type=string,JSONPath=`.spec.githubAPI.host` +// +kubebuilder:printcolumn:name="Status",type=string,JSONPath=`.status.phase` +// +kubebuilder:printcolumn:name="Age",type=date,JSONPath=`.metadata.creationTimestamp` +// +kubebuilder:subresource:status +type GitHubIdentityProvider struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + // Spec for configuring the identity provider. + Spec GitHubIdentityProviderSpec `json:"spec"` + + // Status of the identity provider. + Status GitHubIdentityProviderStatus `json:"status,omitempty"` +} + +// GitHubIdentityProviderList lists GitHubIdentityProvider objects. +// +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object +type GitHubIdentityProviderList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + + Items []GitHubIdentityProvider `json:"items"` +} diff --git a/generated/1.24/apis/supervisor/idp/v1alpha1/types_tls.go b/generated/1.24/apis/supervisor/idp/v1alpha1/types_tls.go index 1413a262c..49b49373c 100644 --- a/generated/1.24/apis/supervisor/idp/v1alpha1/types_tls.go +++ b/generated/1.24/apis/supervisor/idp/v1alpha1/types_tls.go @@ -1,9 +1,9 @@ -// Copyright 2020-2022 the Pinniped contributors. All Rights Reserved. +// Copyright 2020-2024 the Pinniped contributors. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 package v1alpha1 -// Configuration for TLS parameters related to identity provider integration. +// TLSSpec provides TLS configuration for identity provider integration. type TLSSpec struct { // X.509 Certificate Authority (base64-encoded PEM bundle). If omitted, a default set of system roots will be trusted. // +optional diff --git a/generated/1.24/apis/supervisor/idp/v1alpha1/zz_generated.deepcopy.go b/generated/1.24/apis/supervisor/idp/v1alpha1/zz_generated.deepcopy.go index b0cb168b5..e48860e82 100644 --- a/generated/1.24/apis/supervisor/idp/v1alpha1/zz_generated.deepcopy.go +++ b/generated/1.24/apis/supervisor/idp/v1alpha1/zz_generated.deepcopy.go @@ -203,6 +203,221 @@ func (in *ActiveDirectoryIdentityProviderUserSearchAttributes) DeepCopy() *Activ return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *GitHubAPIConfig) DeepCopyInto(out *GitHubAPIConfig) { + *out = *in + if in.Host != nil { + in, out := &in.Host, &out.Host + *out = new(string) + **out = **in + } + if in.TLS != nil { + in, out := &in.TLS, &out.TLS + *out = new(TLSSpec) + **out = **in + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GitHubAPIConfig. +func (in *GitHubAPIConfig) DeepCopy() *GitHubAPIConfig { + if in == nil { + return nil + } + out := new(GitHubAPIConfig) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *GitHubAllowAuthenticationSpec) DeepCopyInto(out *GitHubAllowAuthenticationSpec) { + *out = *in + in.Organizations.DeepCopyInto(&out.Organizations) + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GitHubAllowAuthenticationSpec. +func (in *GitHubAllowAuthenticationSpec) DeepCopy() *GitHubAllowAuthenticationSpec { + if in == nil { + return nil + } + out := new(GitHubAllowAuthenticationSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *GitHubClaims) DeepCopyInto(out *GitHubClaims) { + *out = *in + if in.Username != nil { + in, out := &in.Username, &out.Username + *out = new(GitHubUsernameAttribute) + **out = **in + } + if in.Groups != nil { + in, out := &in.Groups, &out.Groups + *out = new(GitHubGroupNameAttribute) + **out = **in + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GitHubClaims. +func (in *GitHubClaims) DeepCopy() *GitHubClaims { + if in == nil { + return nil + } + out := new(GitHubClaims) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *GitHubClientSpec) DeepCopyInto(out *GitHubClientSpec) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GitHubClientSpec. +func (in *GitHubClientSpec) DeepCopy() *GitHubClientSpec { + if in == nil { + return nil + } + out := new(GitHubClientSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *GitHubIdentityProvider) DeepCopyInto(out *GitHubIdentityProvider) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + in.Status.DeepCopyInto(&out.Status) + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GitHubIdentityProvider. +func (in *GitHubIdentityProvider) DeepCopy() *GitHubIdentityProvider { + if in == nil { + return nil + } + out := new(GitHubIdentityProvider) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *GitHubIdentityProvider) 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 *GitHubIdentityProviderList) DeepCopyInto(out *GitHubIdentityProviderList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]GitHubIdentityProvider, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GitHubIdentityProviderList. +func (in *GitHubIdentityProviderList) DeepCopy() *GitHubIdentityProviderList { + if in == nil { + return nil + } + out := new(GitHubIdentityProviderList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *GitHubIdentityProviderList) 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 *GitHubIdentityProviderSpec) DeepCopyInto(out *GitHubIdentityProviderSpec) { + *out = *in + in.GitHubAPI.DeepCopyInto(&out.GitHubAPI) + in.Claims.DeepCopyInto(&out.Claims) + in.AllowAuthentication.DeepCopyInto(&out.AllowAuthentication) + out.Client = in.Client + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GitHubIdentityProviderSpec. +func (in *GitHubIdentityProviderSpec) DeepCopy() *GitHubIdentityProviderSpec { + if in == nil { + return nil + } + out := new(GitHubIdentityProviderSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *GitHubIdentityProviderStatus) DeepCopyInto(out *GitHubIdentityProviderStatus) { + *out = *in + if in.Conditions != nil { + in, out := &in.Conditions, &out.Conditions + *out = make([]v1.Condition, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GitHubIdentityProviderStatus. +func (in *GitHubIdentityProviderStatus) DeepCopy() *GitHubIdentityProviderStatus { + if in == nil { + return nil + } + out := new(GitHubIdentityProviderStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *GitHubOrganizationsSpec) DeepCopyInto(out *GitHubOrganizationsSpec) { + *out = *in + if in.Policy != nil { + in, out := &in.Policy, &out.Policy + *out = new(GitHubAllowedAuthOrganizationsPolicy) + **out = **in + } + if in.Allowed != nil { + in, out := &in.Allowed, &out.Allowed + *out = make([]string, len(*in)) + copy(*out, *in) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GitHubOrganizationsSpec. +func (in *GitHubOrganizationsSpec) DeepCopy() *GitHubOrganizationsSpec { + if in == nil { + return nil + } + out := new(GitHubOrganizationsSpec) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *LDAPIdentityProvider) DeepCopyInto(out *LDAPIdentityProvider) { *out = *in diff --git a/generated/1.24/apis/supervisor/idpdiscovery/v1alpha1/types_supervisor_idp_discovery.go b/generated/1.24/apis/supervisor/idpdiscovery/v1alpha1/types_supervisor_idp_discovery.go index 837fed2dd..11be6f9dc 100644 --- a/generated/1.24/apis/supervisor/idpdiscovery/v1alpha1/types_supervisor_idp_discovery.go +++ b/generated/1.24/apis/supervisor/idpdiscovery/v1alpha1/types_supervisor_idp_discovery.go @@ -15,6 +15,7 @@ const ( IDPTypeOIDC IDPType = "oidc" IDPTypeLDAP IDPType = "ldap" IDPTypeActiveDirectory IDPType = "activedirectory" + IDPTypeGitHub IDPType = "github" IDPFlowCLIPassword IDPFlow = "cli_password" IDPFlowBrowserAuthcode IDPFlow = "browser_authcode" diff --git a/generated/1.24/client/supervisor/clientset/versioned/typed/idp/v1alpha1/fake/fake_githubidentityprovider.go b/generated/1.24/client/supervisor/clientset/versioned/typed/idp/v1alpha1/fake/fake_githubidentityprovider.go new file mode 100644 index 000000000..d93e826c1 --- /dev/null +++ b/generated/1.24/client/supervisor/clientset/versioned/typed/idp/v1alpha1/fake/fake_githubidentityprovider.go @@ -0,0 +1,129 @@ +// Copyright 2020-2024 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +// Code generated by client-gen. DO NOT EDIT. + +package fake + +import ( + "context" + + v1alpha1 "go.pinniped.dev/generated/1.24/apis/supervisor/idp/v1alpha1" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + labels "k8s.io/apimachinery/pkg/labels" + schema "k8s.io/apimachinery/pkg/runtime/schema" + types "k8s.io/apimachinery/pkg/types" + watch "k8s.io/apimachinery/pkg/watch" + testing "k8s.io/client-go/testing" +) + +// FakeGitHubIdentityProviders implements GitHubIdentityProviderInterface +type FakeGitHubIdentityProviders struct { + Fake *FakeIDPV1alpha1 + ns string +} + +var githubidentityprovidersResource = schema.GroupVersionResource{Group: "idp.supervisor.pinniped.dev", Version: "v1alpha1", Resource: "githubidentityproviders"} + +var githubidentityprovidersKind = schema.GroupVersionKind{Group: "idp.supervisor.pinniped.dev", Version: "v1alpha1", Kind: "GitHubIdentityProvider"} + +// Get takes name of the gitHubIdentityProvider, and returns the corresponding gitHubIdentityProvider object, and an error if there is any. +func (c *FakeGitHubIdentityProviders) Get(ctx context.Context, name string, options v1.GetOptions) (result *v1alpha1.GitHubIdentityProvider, err error) { + obj, err := c.Fake. + Invokes(testing.NewGetAction(githubidentityprovidersResource, c.ns, name), &v1alpha1.GitHubIdentityProvider{}) + + if obj == nil { + return nil, err + } + return obj.(*v1alpha1.GitHubIdentityProvider), err +} + +// List takes label and field selectors, and returns the list of GitHubIdentityProviders that match those selectors. +func (c *FakeGitHubIdentityProviders) List(ctx context.Context, opts v1.ListOptions) (result *v1alpha1.GitHubIdentityProviderList, err error) { + obj, err := c.Fake. + Invokes(testing.NewListAction(githubidentityprovidersResource, githubidentityprovidersKind, c.ns, opts), &v1alpha1.GitHubIdentityProviderList{}) + + if obj == nil { + return nil, err + } + + label, _, _ := testing.ExtractFromListOptions(opts) + if label == nil { + label = labels.Everything() + } + list := &v1alpha1.GitHubIdentityProviderList{ListMeta: obj.(*v1alpha1.GitHubIdentityProviderList).ListMeta} + for _, item := range obj.(*v1alpha1.GitHubIdentityProviderList).Items { + if label.Matches(labels.Set(item.Labels)) { + list.Items = append(list.Items, item) + } + } + return list, err +} + +// Watch returns a watch.Interface that watches the requested gitHubIdentityProviders. +func (c *FakeGitHubIdentityProviders) Watch(ctx context.Context, opts v1.ListOptions) (watch.Interface, error) { + return c.Fake. + InvokesWatch(testing.NewWatchAction(githubidentityprovidersResource, c.ns, opts)) + +} + +// Create takes the representation of a gitHubIdentityProvider and creates it. Returns the server's representation of the gitHubIdentityProvider, and an error, if there is any. +func (c *FakeGitHubIdentityProviders) Create(ctx context.Context, gitHubIdentityProvider *v1alpha1.GitHubIdentityProvider, opts v1.CreateOptions) (result *v1alpha1.GitHubIdentityProvider, err error) { + obj, err := c.Fake. + Invokes(testing.NewCreateAction(githubidentityprovidersResource, c.ns, gitHubIdentityProvider), &v1alpha1.GitHubIdentityProvider{}) + + if obj == nil { + return nil, err + } + return obj.(*v1alpha1.GitHubIdentityProvider), err +} + +// Update takes the representation of a gitHubIdentityProvider and updates it. Returns the server's representation of the gitHubIdentityProvider, and an error, if there is any. +func (c *FakeGitHubIdentityProviders) Update(ctx context.Context, gitHubIdentityProvider *v1alpha1.GitHubIdentityProvider, opts v1.UpdateOptions) (result *v1alpha1.GitHubIdentityProvider, err error) { + obj, err := c.Fake. + Invokes(testing.NewUpdateAction(githubidentityprovidersResource, c.ns, gitHubIdentityProvider), &v1alpha1.GitHubIdentityProvider{}) + + if obj == nil { + return nil, err + } + return obj.(*v1alpha1.GitHubIdentityProvider), err +} + +// UpdateStatus was generated because the type contains a Status member. +// Add a +genclient:noStatus comment above the type to avoid generating UpdateStatus(). +func (c *FakeGitHubIdentityProviders) UpdateStatus(ctx context.Context, gitHubIdentityProvider *v1alpha1.GitHubIdentityProvider, opts v1.UpdateOptions) (*v1alpha1.GitHubIdentityProvider, error) { + obj, err := c.Fake. + Invokes(testing.NewUpdateSubresourceAction(githubidentityprovidersResource, "status", c.ns, gitHubIdentityProvider), &v1alpha1.GitHubIdentityProvider{}) + + if obj == nil { + return nil, err + } + return obj.(*v1alpha1.GitHubIdentityProvider), err +} + +// Delete takes name of the gitHubIdentityProvider and deletes it. Returns an error if one occurs. +func (c *FakeGitHubIdentityProviders) Delete(ctx context.Context, name string, opts v1.DeleteOptions) error { + _, err := c.Fake. + Invokes(testing.NewDeleteActionWithOptions(githubidentityprovidersResource, c.ns, name, opts), &v1alpha1.GitHubIdentityProvider{}) + + return err +} + +// DeleteCollection deletes a collection of objects. +func (c *FakeGitHubIdentityProviders) DeleteCollection(ctx context.Context, opts v1.DeleteOptions, listOpts v1.ListOptions) error { + action := testing.NewDeleteCollectionAction(githubidentityprovidersResource, c.ns, listOpts) + + _, err := c.Fake.Invokes(action, &v1alpha1.GitHubIdentityProviderList{}) + return err +} + +// Patch applies the patch and returns the patched gitHubIdentityProvider. +func (c *FakeGitHubIdentityProviders) Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts v1.PatchOptions, subresources ...string) (result *v1alpha1.GitHubIdentityProvider, err error) { + obj, err := c.Fake. + Invokes(testing.NewPatchSubresourceAction(githubidentityprovidersResource, c.ns, name, pt, data, subresources...), &v1alpha1.GitHubIdentityProvider{}) + + if obj == nil { + return nil, err + } + return obj.(*v1alpha1.GitHubIdentityProvider), err +} diff --git a/generated/1.24/client/supervisor/clientset/versioned/typed/idp/v1alpha1/fake/fake_idp_client.go b/generated/1.24/client/supervisor/clientset/versioned/typed/idp/v1alpha1/fake/fake_idp_client.go index c4da0d643..daa138783 100644 --- a/generated/1.24/client/supervisor/clientset/versioned/typed/idp/v1alpha1/fake/fake_idp_client.go +++ b/generated/1.24/client/supervisor/clientset/versioned/typed/idp/v1alpha1/fake/fake_idp_client.go @@ -19,6 +19,10 @@ func (c *FakeIDPV1alpha1) ActiveDirectoryIdentityProviders(namespace string) v1a return &FakeActiveDirectoryIdentityProviders{c, namespace} } +func (c *FakeIDPV1alpha1) GitHubIdentityProviders(namespace string) v1alpha1.GitHubIdentityProviderInterface { + return &FakeGitHubIdentityProviders{c, namespace} +} + func (c *FakeIDPV1alpha1) LDAPIdentityProviders(namespace string) v1alpha1.LDAPIdentityProviderInterface { return &FakeLDAPIdentityProviders{c, namespace} } diff --git a/generated/1.24/client/supervisor/clientset/versioned/typed/idp/v1alpha1/generated_expansion.go b/generated/1.24/client/supervisor/clientset/versioned/typed/idp/v1alpha1/generated_expansion.go index 79be098d6..a7acc8120 100644 --- a/generated/1.24/client/supervisor/clientset/versioned/typed/idp/v1alpha1/generated_expansion.go +++ b/generated/1.24/client/supervisor/clientset/versioned/typed/idp/v1alpha1/generated_expansion.go @@ -7,6 +7,8 @@ package v1alpha1 type ActiveDirectoryIdentityProviderExpansion interface{} +type GitHubIdentityProviderExpansion interface{} + type LDAPIdentityProviderExpansion interface{} type OIDCIdentityProviderExpansion interface{} diff --git a/generated/1.24/client/supervisor/clientset/versioned/typed/idp/v1alpha1/githubidentityprovider.go b/generated/1.24/client/supervisor/clientset/versioned/typed/idp/v1alpha1/githubidentityprovider.go new file mode 100644 index 000000000..2ee66b57f --- /dev/null +++ b/generated/1.24/client/supervisor/clientset/versioned/typed/idp/v1alpha1/githubidentityprovider.go @@ -0,0 +1,182 @@ +// Copyright 2020-2024 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +// Code generated by client-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + "context" + "time" + + v1alpha1 "go.pinniped.dev/generated/1.24/apis/supervisor/idp/v1alpha1" + scheme "go.pinniped.dev/generated/1.24/client/supervisor/clientset/versioned/scheme" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + types "k8s.io/apimachinery/pkg/types" + watch "k8s.io/apimachinery/pkg/watch" + rest "k8s.io/client-go/rest" +) + +// GitHubIdentityProvidersGetter has a method to return a GitHubIdentityProviderInterface. +// A group's client should implement this interface. +type GitHubIdentityProvidersGetter interface { + GitHubIdentityProviders(namespace string) GitHubIdentityProviderInterface +} + +// GitHubIdentityProviderInterface has methods to work with GitHubIdentityProvider resources. +type GitHubIdentityProviderInterface interface { + Create(ctx context.Context, gitHubIdentityProvider *v1alpha1.GitHubIdentityProvider, opts v1.CreateOptions) (*v1alpha1.GitHubIdentityProvider, error) + Update(ctx context.Context, gitHubIdentityProvider *v1alpha1.GitHubIdentityProvider, opts v1.UpdateOptions) (*v1alpha1.GitHubIdentityProvider, error) + UpdateStatus(ctx context.Context, gitHubIdentityProvider *v1alpha1.GitHubIdentityProvider, opts v1.UpdateOptions) (*v1alpha1.GitHubIdentityProvider, 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) (*v1alpha1.GitHubIdentityProvider, error) + List(ctx context.Context, opts v1.ListOptions) (*v1alpha1.GitHubIdentityProviderList, 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 *v1alpha1.GitHubIdentityProvider, err error) + GitHubIdentityProviderExpansion +} + +// gitHubIdentityProviders implements GitHubIdentityProviderInterface +type gitHubIdentityProviders struct { + client rest.Interface + ns string +} + +// newGitHubIdentityProviders returns a GitHubIdentityProviders +func newGitHubIdentityProviders(c *IDPV1alpha1Client, namespace string) *gitHubIdentityProviders { + return &gitHubIdentityProviders{ + client: c.RESTClient(), + ns: namespace, + } +} + +// Get takes name of the gitHubIdentityProvider, and returns the corresponding gitHubIdentityProvider object, and an error if there is any. +func (c *gitHubIdentityProviders) Get(ctx context.Context, name string, options v1.GetOptions) (result *v1alpha1.GitHubIdentityProvider, err error) { + result = &v1alpha1.GitHubIdentityProvider{} + err = c.client.Get(). + Namespace(c.ns). + Resource("githubidentityproviders"). + Name(name). + VersionedParams(&options, scheme.ParameterCodec). + Do(ctx). + Into(result) + return +} + +// List takes label and field selectors, and returns the list of GitHubIdentityProviders that match those selectors. +func (c *gitHubIdentityProviders) List(ctx context.Context, opts v1.ListOptions) (result *v1alpha1.GitHubIdentityProviderList, err error) { + var timeout time.Duration + if opts.TimeoutSeconds != nil { + timeout = time.Duration(*opts.TimeoutSeconds) * time.Second + } + result = &v1alpha1.GitHubIdentityProviderList{} + err = c.client.Get(). + Namespace(c.ns). + Resource("githubidentityproviders"). + VersionedParams(&opts, scheme.ParameterCodec). + Timeout(timeout). + Do(ctx). + Into(result) + return +} + +// Watch returns a watch.Interface that watches the requested gitHubIdentityProviders. +func (c *gitHubIdentityProviders) Watch(ctx context.Context, opts v1.ListOptions) (watch.Interface, error) { + var timeout time.Duration + if opts.TimeoutSeconds != nil { + timeout = time.Duration(*opts.TimeoutSeconds) * time.Second + } + opts.Watch = true + return c.client.Get(). + Namespace(c.ns). + Resource("githubidentityproviders"). + VersionedParams(&opts, scheme.ParameterCodec). + Timeout(timeout). + Watch(ctx) +} + +// Create takes the representation of a gitHubIdentityProvider and creates it. Returns the server's representation of the gitHubIdentityProvider, and an error, if there is any. +func (c *gitHubIdentityProviders) Create(ctx context.Context, gitHubIdentityProvider *v1alpha1.GitHubIdentityProvider, opts v1.CreateOptions) (result *v1alpha1.GitHubIdentityProvider, err error) { + result = &v1alpha1.GitHubIdentityProvider{} + err = c.client.Post(). + Namespace(c.ns). + Resource("githubidentityproviders"). + VersionedParams(&opts, scheme.ParameterCodec). + Body(gitHubIdentityProvider). + Do(ctx). + Into(result) + return +} + +// Update takes the representation of a gitHubIdentityProvider and updates it. Returns the server's representation of the gitHubIdentityProvider, and an error, if there is any. +func (c *gitHubIdentityProviders) Update(ctx context.Context, gitHubIdentityProvider *v1alpha1.GitHubIdentityProvider, opts v1.UpdateOptions) (result *v1alpha1.GitHubIdentityProvider, err error) { + result = &v1alpha1.GitHubIdentityProvider{} + err = c.client.Put(). + Namespace(c.ns). + Resource("githubidentityproviders"). + Name(gitHubIdentityProvider.Name). + VersionedParams(&opts, scheme.ParameterCodec). + Body(gitHubIdentityProvider). + Do(ctx). + Into(result) + return +} + +// UpdateStatus was generated because the type contains a Status member. +// Add a +genclient:noStatus comment above the type to avoid generating UpdateStatus(). +func (c *gitHubIdentityProviders) UpdateStatus(ctx context.Context, gitHubIdentityProvider *v1alpha1.GitHubIdentityProvider, opts v1.UpdateOptions) (result *v1alpha1.GitHubIdentityProvider, err error) { + result = &v1alpha1.GitHubIdentityProvider{} + err = c.client.Put(). + Namespace(c.ns). + Resource("githubidentityproviders"). + Name(gitHubIdentityProvider.Name). + SubResource("status"). + VersionedParams(&opts, scheme.ParameterCodec). + Body(gitHubIdentityProvider). + Do(ctx). + Into(result) + return +} + +// Delete takes name of the gitHubIdentityProvider and deletes it. Returns an error if one occurs. +func (c *gitHubIdentityProviders) Delete(ctx context.Context, name string, opts v1.DeleteOptions) error { + return c.client.Delete(). + Namespace(c.ns). + Resource("githubidentityproviders"). + Name(name). + Body(&opts). + Do(ctx). + Error() +} + +// DeleteCollection deletes a collection of objects. +func (c *gitHubIdentityProviders) DeleteCollection(ctx context.Context, opts v1.DeleteOptions, listOpts v1.ListOptions) error { + var timeout time.Duration + if listOpts.TimeoutSeconds != nil { + timeout = time.Duration(*listOpts.TimeoutSeconds) * time.Second + } + return c.client.Delete(). + Namespace(c.ns). + Resource("githubidentityproviders"). + VersionedParams(&listOpts, scheme.ParameterCodec). + Timeout(timeout). + Body(&opts). + Do(ctx). + Error() +} + +// Patch applies the patch and returns the patched gitHubIdentityProvider. +func (c *gitHubIdentityProviders) Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts v1.PatchOptions, subresources ...string) (result *v1alpha1.GitHubIdentityProvider, err error) { + result = &v1alpha1.GitHubIdentityProvider{} + err = c.client.Patch(pt). + Namespace(c.ns). + Resource("githubidentityproviders"). + Name(name). + SubResource(subresources...). + VersionedParams(&opts, scheme.ParameterCodec). + Body(data). + Do(ctx). + Into(result) + return +} diff --git a/generated/1.24/client/supervisor/clientset/versioned/typed/idp/v1alpha1/idp_client.go b/generated/1.24/client/supervisor/clientset/versioned/typed/idp/v1alpha1/idp_client.go index 697a152c1..8a72bfb69 100644 --- a/generated/1.24/client/supervisor/clientset/versioned/typed/idp/v1alpha1/idp_client.go +++ b/generated/1.24/client/supervisor/clientset/versioned/typed/idp/v1alpha1/idp_client.go @@ -16,6 +16,7 @@ import ( type IDPV1alpha1Interface interface { RESTClient() rest.Interface ActiveDirectoryIdentityProvidersGetter + GitHubIdentityProvidersGetter LDAPIdentityProvidersGetter OIDCIdentityProvidersGetter } @@ -29,6 +30,10 @@ func (c *IDPV1alpha1Client) ActiveDirectoryIdentityProviders(namespace string) A return newActiveDirectoryIdentityProviders(c, namespace) } +func (c *IDPV1alpha1Client) GitHubIdentityProviders(namespace string) GitHubIdentityProviderInterface { + return newGitHubIdentityProviders(c, namespace) +} + func (c *IDPV1alpha1Client) LDAPIdentityProviders(namespace string) LDAPIdentityProviderInterface { return newLDAPIdentityProviders(c, namespace) } diff --git a/generated/1.24/client/supervisor/informers/externalversions/generic.go b/generated/1.24/client/supervisor/informers/externalversions/generic.go index 1a4058e49..2f28e5356 100644 --- a/generated/1.24/client/supervisor/informers/externalversions/generic.go +++ b/generated/1.24/client/supervisor/informers/externalversions/generic.go @@ -49,6 +49,8 @@ func (f *sharedInformerFactory) ForResource(resource schema.GroupVersionResource // Group=idp.supervisor.pinniped.dev, Version=v1alpha1 case idpv1alpha1.SchemeGroupVersion.WithResource("activedirectoryidentityproviders"): return &genericInformer{resource: resource.GroupResource(), informer: f.IDP().V1alpha1().ActiveDirectoryIdentityProviders().Informer()}, nil + case idpv1alpha1.SchemeGroupVersion.WithResource("githubidentityproviders"): + return &genericInformer{resource: resource.GroupResource(), informer: f.IDP().V1alpha1().GitHubIdentityProviders().Informer()}, nil case idpv1alpha1.SchemeGroupVersion.WithResource("ldapidentityproviders"): return &genericInformer{resource: resource.GroupResource(), informer: f.IDP().V1alpha1().LDAPIdentityProviders().Informer()}, nil case idpv1alpha1.SchemeGroupVersion.WithResource("oidcidentityproviders"): diff --git a/generated/1.24/client/supervisor/informers/externalversions/idp/v1alpha1/githubidentityprovider.go b/generated/1.24/client/supervisor/informers/externalversions/idp/v1alpha1/githubidentityprovider.go new file mode 100644 index 000000000..c38cf993b --- /dev/null +++ b/generated/1.24/client/supervisor/informers/externalversions/idp/v1alpha1/githubidentityprovider.go @@ -0,0 +1,77 @@ +// Copyright 2020-2024 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +// Code generated by informer-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + "context" + time "time" + + idpv1alpha1 "go.pinniped.dev/generated/1.24/apis/supervisor/idp/v1alpha1" + versioned "go.pinniped.dev/generated/1.24/client/supervisor/clientset/versioned" + internalinterfaces "go.pinniped.dev/generated/1.24/client/supervisor/informers/externalversions/internalinterfaces" + v1alpha1 "go.pinniped.dev/generated/1.24/client/supervisor/listers/idp/v1alpha1" + 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" +) + +// GitHubIdentityProviderInformer provides access to a shared informer and lister for +// GitHubIdentityProviders. +type GitHubIdentityProviderInformer interface { + Informer() cache.SharedIndexInformer + Lister() v1alpha1.GitHubIdentityProviderLister +} + +type gitHubIdentityProviderInformer struct { + factory internalinterfaces.SharedInformerFactory + tweakListOptions internalinterfaces.TweakListOptionsFunc + namespace string +} + +// NewGitHubIdentityProviderInformer constructs a new informer for GitHubIdentityProvider 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 NewGitHubIdentityProviderInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers) cache.SharedIndexInformer { + return NewFilteredGitHubIdentityProviderInformer(client, namespace, resyncPeriod, indexers, nil) +} + +// NewFilteredGitHubIdentityProviderInformer constructs a new informer for GitHubIdentityProvider 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 NewFilteredGitHubIdentityProviderInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers, tweakListOptions internalinterfaces.TweakListOptionsFunc) cache.SharedIndexInformer { + return cache.NewSharedIndexInformer( + &cache.ListWatch{ + ListFunc: func(options v1.ListOptions) (runtime.Object, error) { + if tweakListOptions != nil { + tweakListOptions(&options) + } + return client.IDPV1alpha1().GitHubIdentityProviders(namespace).List(context.TODO(), options) + }, + WatchFunc: func(options v1.ListOptions) (watch.Interface, error) { + if tweakListOptions != nil { + tweakListOptions(&options) + } + return client.IDPV1alpha1().GitHubIdentityProviders(namespace).Watch(context.TODO(), options) + }, + }, + &idpv1alpha1.GitHubIdentityProvider{}, + resyncPeriod, + indexers, + ) +} + +func (f *gitHubIdentityProviderInformer) defaultInformer(client versioned.Interface, resyncPeriod time.Duration) cache.SharedIndexInformer { + return NewFilteredGitHubIdentityProviderInformer(client, f.namespace, resyncPeriod, cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc}, f.tweakListOptions) +} + +func (f *gitHubIdentityProviderInformer) Informer() cache.SharedIndexInformer { + return f.factory.InformerFor(&idpv1alpha1.GitHubIdentityProvider{}, f.defaultInformer) +} + +func (f *gitHubIdentityProviderInformer) Lister() v1alpha1.GitHubIdentityProviderLister { + return v1alpha1.NewGitHubIdentityProviderLister(f.Informer().GetIndexer()) +} diff --git a/generated/1.24/client/supervisor/informers/externalversions/idp/v1alpha1/interface.go b/generated/1.24/client/supervisor/informers/externalversions/idp/v1alpha1/interface.go index b7cf724a6..32df7ee81 100644 --- a/generated/1.24/client/supervisor/informers/externalversions/idp/v1alpha1/interface.go +++ b/generated/1.24/client/supervisor/informers/externalversions/idp/v1alpha1/interface.go @@ -13,6 +13,8 @@ import ( type Interface interface { // ActiveDirectoryIdentityProviders returns a ActiveDirectoryIdentityProviderInformer. ActiveDirectoryIdentityProviders() ActiveDirectoryIdentityProviderInformer + // GitHubIdentityProviders returns a GitHubIdentityProviderInformer. + GitHubIdentityProviders() GitHubIdentityProviderInformer // LDAPIdentityProviders returns a LDAPIdentityProviderInformer. LDAPIdentityProviders() LDAPIdentityProviderInformer // OIDCIdentityProviders returns a OIDCIdentityProviderInformer. @@ -35,6 +37,11 @@ func (v *version) ActiveDirectoryIdentityProviders() ActiveDirectoryIdentityProv return &activeDirectoryIdentityProviderInformer{factory: v.factory, namespace: v.namespace, tweakListOptions: v.tweakListOptions} } +// GitHubIdentityProviders returns a GitHubIdentityProviderInformer. +func (v *version) GitHubIdentityProviders() GitHubIdentityProviderInformer { + return &gitHubIdentityProviderInformer{factory: v.factory, namespace: v.namespace, tweakListOptions: v.tweakListOptions} +} + // LDAPIdentityProviders returns a LDAPIdentityProviderInformer. func (v *version) LDAPIdentityProviders() LDAPIdentityProviderInformer { return &lDAPIdentityProviderInformer{factory: v.factory, namespace: v.namespace, tweakListOptions: v.tweakListOptions} diff --git a/generated/1.24/client/supervisor/listers/idp/v1alpha1/expansion_generated.go b/generated/1.24/client/supervisor/listers/idp/v1alpha1/expansion_generated.go index b491a9e8d..470c06abb 100644 --- a/generated/1.24/client/supervisor/listers/idp/v1alpha1/expansion_generated.go +++ b/generated/1.24/client/supervisor/listers/idp/v1alpha1/expansion_generated.go @@ -13,6 +13,14 @@ type ActiveDirectoryIdentityProviderListerExpansion interface{} // ActiveDirectoryIdentityProviderNamespaceLister. type ActiveDirectoryIdentityProviderNamespaceListerExpansion interface{} +// GitHubIdentityProviderListerExpansion allows custom methods to be added to +// GitHubIdentityProviderLister. +type GitHubIdentityProviderListerExpansion interface{} + +// GitHubIdentityProviderNamespaceListerExpansion allows custom methods to be added to +// GitHubIdentityProviderNamespaceLister. +type GitHubIdentityProviderNamespaceListerExpansion interface{} + // LDAPIdentityProviderListerExpansion allows custom methods to be added to // LDAPIdentityProviderLister. type LDAPIdentityProviderListerExpansion interface{} diff --git a/generated/1.24/client/supervisor/listers/idp/v1alpha1/githubidentityprovider.go b/generated/1.24/client/supervisor/listers/idp/v1alpha1/githubidentityprovider.go new file mode 100644 index 000000000..b1364368a --- /dev/null +++ b/generated/1.24/client/supervisor/listers/idp/v1alpha1/githubidentityprovider.go @@ -0,0 +1,86 @@ +// Copyright 2020-2024 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +// Code generated by lister-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + v1alpha1 "go.pinniped.dev/generated/1.24/apis/supervisor/idp/v1alpha1" + "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/client-go/tools/cache" +) + +// GitHubIdentityProviderLister helps list GitHubIdentityProviders. +// All objects returned here must be treated as read-only. +type GitHubIdentityProviderLister interface { + // List lists all GitHubIdentityProviders in the indexer. + // Objects returned here must be treated as read-only. + List(selector labels.Selector) (ret []*v1alpha1.GitHubIdentityProvider, err error) + // GitHubIdentityProviders returns an object that can list and get GitHubIdentityProviders. + GitHubIdentityProviders(namespace string) GitHubIdentityProviderNamespaceLister + GitHubIdentityProviderListerExpansion +} + +// gitHubIdentityProviderLister implements the GitHubIdentityProviderLister interface. +type gitHubIdentityProviderLister struct { + indexer cache.Indexer +} + +// NewGitHubIdentityProviderLister returns a new GitHubIdentityProviderLister. +func NewGitHubIdentityProviderLister(indexer cache.Indexer) GitHubIdentityProviderLister { + return &gitHubIdentityProviderLister{indexer: indexer} +} + +// List lists all GitHubIdentityProviders in the indexer. +func (s *gitHubIdentityProviderLister) List(selector labels.Selector) (ret []*v1alpha1.GitHubIdentityProvider, err error) { + err = cache.ListAll(s.indexer, selector, func(m interface{}) { + ret = append(ret, m.(*v1alpha1.GitHubIdentityProvider)) + }) + return ret, err +} + +// GitHubIdentityProviders returns an object that can list and get GitHubIdentityProviders. +func (s *gitHubIdentityProviderLister) GitHubIdentityProviders(namespace string) GitHubIdentityProviderNamespaceLister { + return gitHubIdentityProviderNamespaceLister{indexer: s.indexer, namespace: namespace} +} + +// GitHubIdentityProviderNamespaceLister helps list and get GitHubIdentityProviders. +// All objects returned here must be treated as read-only. +type GitHubIdentityProviderNamespaceLister interface { + // List lists all GitHubIdentityProviders in the indexer for a given namespace. + // Objects returned here must be treated as read-only. + List(selector labels.Selector) (ret []*v1alpha1.GitHubIdentityProvider, err error) + // Get retrieves the GitHubIdentityProvider from the indexer for a given namespace and name. + // Objects returned here must be treated as read-only. + Get(name string) (*v1alpha1.GitHubIdentityProvider, error) + GitHubIdentityProviderNamespaceListerExpansion +} + +// gitHubIdentityProviderNamespaceLister implements the GitHubIdentityProviderNamespaceLister +// interface. +type gitHubIdentityProviderNamespaceLister struct { + indexer cache.Indexer + namespace string +} + +// List lists all GitHubIdentityProviders in the indexer for a given namespace. +func (s gitHubIdentityProviderNamespaceLister) List(selector labels.Selector) (ret []*v1alpha1.GitHubIdentityProvider, err error) { + err = cache.ListAllByNamespace(s.indexer, s.namespace, selector, func(m interface{}) { + ret = append(ret, m.(*v1alpha1.GitHubIdentityProvider)) + }) + return ret, err +} + +// Get retrieves the GitHubIdentityProvider from the indexer for a given namespace and name. +func (s gitHubIdentityProviderNamespaceLister) Get(name string) (*v1alpha1.GitHubIdentityProvider, error) { + obj, exists, err := s.indexer.GetByKey(s.namespace + "/" + name) + if err != nil { + return nil, err + } + if !exists { + return nil, errors.NewNotFound(v1alpha1.Resource("githubidentityprovider"), name) + } + return obj.(*v1alpha1.GitHubIdentityProvider), nil +} diff --git a/generated/1.24/crds/authentication.concierge.pinniped.dev_jwtauthenticators.yaml b/generated/1.24/crds/authentication.concierge.pinniped.dev_jwtauthenticators.yaml index b464cfa2b..d59fcb783 100644 --- a/generated/1.24/crds/authentication.concierge.pinniped.dev_jwtauthenticators.yaml +++ b/generated/1.24/crds/authentication.concierge.pinniped.dev_jwtauthenticators.yaml @@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - controller-gen.kubebuilder.io/version: v0.14.0 + controller-gen.kubebuilder.io/version: v0.15.0 name: jwtauthenticators.authentication.concierge.pinniped.dev spec: group: authentication.concierge.pinniped.dev diff --git a/generated/1.24/crds/authentication.concierge.pinniped.dev_webhookauthenticators.yaml b/generated/1.24/crds/authentication.concierge.pinniped.dev_webhookauthenticators.yaml index e8cbc6790..4ccd53770 100644 --- a/generated/1.24/crds/authentication.concierge.pinniped.dev_webhookauthenticators.yaml +++ b/generated/1.24/crds/authentication.concierge.pinniped.dev_webhookauthenticators.yaml @@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - controller-gen.kubebuilder.io/version: v0.14.0 + controller-gen.kubebuilder.io/version: v0.15.0 name: webhookauthenticators.authentication.concierge.pinniped.dev spec: group: authentication.concierge.pinniped.dev diff --git a/generated/1.24/crds/config.concierge.pinniped.dev_credentialissuers.yaml b/generated/1.24/crds/config.concierge.pinniped.dev_credentialissuers.yaml index 8d77a6665..26fed7376 100644 --- a/generated/1.24/crds/config.concierge.pinniped.dev_credentialissuers.yaml +++ b/generated/1.24/crds/config.concierge.pinniped.dev_credentialissuers.yaml @@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - controller-gen.kubebuilder.io/version: v0.14.0 + controller-gen.kubebuilder.io/version: v0.15.0 name: credentialissuers.config.concierge.pinniped.dev spec: group: config.concierge.pinniped.dev diff --git a/generated/1.24/crds/config.supervisor.pinniped.dev_federationdomains.yaml b/generated/1.24/crds/config.supervisor.pinniped.dev_federationdomains.yaml index 7bf231443..d43c7406d 100644 --- a/generated/1.24/crds/config.supervisor.pinniped.dev_federationdomains.yaml +++ b/generated/1.24/crds/config.supervisor.pinniped.dev_federationdomains.yaml @@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - controller-gen.kubebuilder.io/version: v0.14.0 + controller-gen.kubebuilder.io/version: v0.15.0 name: federationdomains.config.supervisor.pinniped.dev spec: group: config.supervisor.pinniped.dev diff --git a/generated/1.24/crds/config.supervisor.pinniped.dev_oidcclients.yaml b/generated/1.24/crds/config.supervisor.pinniped.dev_oidcclients.yaml index d33b31bf1..c44ecbf1e 100644 --- a/generated/1.24/crds/config.supervisor.pinniped.dev_oidcclients.yaml +++ b/generated/1.24/crds/config.supervisor.pinniped.dev_oidcclients.yaml @@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - controller-gen.kubebuilder.io/version: v0.14.0 + controller-gen.kubebuilder.io/version: v0.15.0 name: oidcclients.config.supervisor.pinniped.dev spec: group: config.supervisor.pinniped.dev diff --git a/generated/1.24/crds/idp.supervisor.pinniped.dev_activedirectoryidentityproviders.yaml b/generated/1.24/crds/idp.supervisor.pinniped.dev_activedirectoryidentityproviders.yaml index 414ac4f51..062251102 100644 --- a/generated/1.24/crds/idp.supervisor.pinniped.dev_activedirectoryidentityproviders.yaml +++ b/generated/1.24/crds/idp.supervisor.pinniped.dev_activedirectoryidentityproviders.yaml @@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - controller-gen.kubebuilder.io/version: v0.14.0 + controller-gen.kubebuilder.io/version: v0.15.0 name: activedirectoryidentityproviders.idp.supervisor.pinniped.dev spec: group: idp.supervisor.pinniped.dev diff --git a/generated/1.24/crds/idp.supervisor.pinniped.dev_githubidentityproviders.yaml b/generated/1.24/crds/idp.supervisor.pinniped.dev_githubidentityproviders.yaml new file mode 100644 index 000000000..f93108700 --- /dev/null +++ b/generated/1.24/crds/idp.supervisor.pinniped.dev_githubidentityproviders.yaml @@ -0,0 +1,326 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.15.0 + name: githubidentityproviders.idp.supervisor.pinniped.dev +spec: + group: idp.supervisor.pinniped.dev + names: + categories: + - pinniped + - pinniped-idp + - pinniped-idps + kind: GitHubIdentityProvider + listKind: GitHubIdentityProviderList + plural: githubidentityproviders + singular: githubidentityprovider + scope: Namespaced + versions: + - additionalPrinterColumns: + - jsonPath: .spec.githubAPI.host + name: Host + type: string + - jsonPath: .status.phase + name: Status + type: string + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1alpha1 + schema: + openAPIV3Schema: + description: |- + GitHubIdentityProvider describes the configuration of an upstream GitHub identity provider. + This upstream provider can be configured with either a GitHub App or a GitHub OAuth2 App. + + + Right now, only web-based logins are supported, for both the pinniped-cli client and clients configured + as OIDCClients. + 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: + description: Spec for configuring the identity provider. + properties: + allowAuthentication: + description: AllowAuthentication allows customization of who can authenticate + using this IDP and how. + properties: + organizations: + description: Organizations allows customization of which organizations + can authenticate using this IDP. + properties: + allowed: + description: |- + Allowed, when specified, indicates that only users with membership in at least one of the listed + GitHub organizations may log in. In addition, the group membership presented to Kubernetes will only include + teams within the listed GitHub organizations. Additional login rules or group filtering can optionally be + provided as policy expression on any Pinniped Supervisor FederationDomain that includes this IDP. + + + The configured GitHub App or GitHub OAuth App must be allowed to see membership in the listed organizations, + otherwise Pinniped will not be aware that the user belongs to the listed organization or any teams + within that organization. + + + If no organizations are listed, you must set organizations: AllGitHubUsers. + items: + type: string + maxItems: 64 + type: array + x-kubernetes-list-type: set + policy: + default: OnlyUsersFromAllowedOrganizations + description: |- + Policy must be set to "AllGitHubUsers" if allowed is empty. + + + This field only exists to ensure that Pinniped administrators are aware that an empty list of + allowedOrganizations means all GitHub users are allowed to log in. + enum: + - OnlyUsersFromAllowedOrganizations + - AllGitHubUsers + type: string + type: object + x-kubernetes-validations: + - message: spec.allowAuthentication.organizations.policy must + be 'OnlyUsersFromAllowedOrganizations' when spec.allowAuthentication.organizations.allowed + has organizations listed + rule: '!(has(self.allowed) && size(self.allowed) > 0 && self.policy + == ''AllGitHubUsers'')' + - message: spec.allowAuthentication.organizations.policy must + be 'AllGitHubUsers' when spec.allowAuthentication.organizations.allowed + is empty + rule: '!((!has(self.allowed) || size(self.allowed) == 0) && + self.policy == ''OnlyUsersFromAllowedOrganizations'')' + required: + - organizations + type: object + claims: + default: {} + description: Claims allows customization of the username and groups + claims. + properties: + groups: + default: slug + description: |- + Groups configures which property of the GitHub team record shall determine the group names in Kubernetes. + + + Can be either "name" or "slug". Defaults to "slug". + + + GitHub team names can contain upper and lower case characters, whitespace, and punctuation (e.g. "Kube admins!"). + + + GitHub team slugs are lower case alphanumeric characters and may contain dashes and underscores (e.g. "kube-admins"). + + + Group names as presented to Kubernetes will always be prefixed by the GitHub organization name followed by a + forward slash (e.g. "my-org/my-team"). GitHub organization login names can only contain alphanumeric characters + or single hyphens, so the first forward slash `/` will be the separator between the organization login name and + the team name or slug. + + + If desired, an admin could configure identity transformation expressions on the Pinniped Supervisor's + FederationDomain to further customize how these group names are presented to Kubernetes. + + + See the response schema for + [List teams for the authenticated user](https://docs.github.com/en/rest/teams/teams?apiVersion=2022-11-28#list-teams-for-the-authenticated-user). + enum: + - name + - slug + type: string + username: + default: login:id + description: |- + Username configures which property of the GitHub user record shall determine the username in Kubernetes. + + + Can be either "id", "login", or "login:id". Defaults to "login:id". + + + GitHub's user login attributes can only contain alphanumeric characters and non-repeating hyphens, + and may not start or end with hyphens. GitHub users are allowed to change their login name, + although it is inconvenient. If a GitHub user changed their login name from "foo" to "bar", + then a second user might change their name from "baz" to "foo" in order to take the old + username of the first user. For this reason, it is not as safe to make authorization decisions + based only on the user's login attribute. + + + If desired, an admin could configure identity transformation expressions on the Pinniped Supervisor's + FederationDomain to further customize how these usernames are presented to Kubernetes. + + + Defaults to "login:id", which is the user login attribute, followed by a colon, followed by the unique and + unchanging integer ID number attribute. This blends human-readable login names with the unchanging ID value + from GitHub. Colons are not allowed in GitHub login attributes or ID numbers, so this is a reasonable + choice to concatenate the two values. + + + See the response schema for + [Get the authenticated user](https://docs.github.com/en/rest/users/users?apiVersion=2022-11-28#get-the-authenticated-user). + enum: + - id + - login + - login:id + type: string + type: object + client: + description: Client identifies the secret with credentials for a GitHub + App or GitHub OAuth2 App (a GitHub client). + properties: + secretName: + description: |- + SecretName contains the name of a namespace-local Secret object that provides the clientID and + clientSecret for an GitHub App or GitHub OAuth2 client. + + + This secret must be of type "secrets.pinniped.dev/github-client" with keys "clientID" and "clientSecret". + minLength: 1 + type: string + required: + - secretName + type: object + githubAPI: + default: {} + description: GitHubAPI allows configuration for GitHub Enterprise + Server + properties: + host: + default: github.com + description: |- + Host is required only for GitHub Enterprise Server. + Defaults to using GitHub's public API ("github.com"). + Do not specify a protocol or scheme since "https://" will always be used. + Port is optional. Do not specify a path, query, fragment, or userinfo. + Only domain name or IP address, subdomains (optional), and port (optional). + IPv4 and IPv6 are supported. If using an IPv6 address with a port, you must enclose the IPv6 address + in square brackets. Example: "[::1]:443". + minLength: 1 + type: string + tls: + description: TLS configuration for GitHub Enterprise Server. + properties: + certificateAuthorityData: + description: X.509 Certificate Authority (base64-encoded PEM + bundle). If omitted, a default set of system roots will + be trusted. + type: string + type: object + type: object + required: + - allowAuthentication + - client + type: object + status: + description: Status of the identity provider. + properties: + conditions: + description: Conditions represents the observations of an identity + provider's current state. + items: + description: "Condition contains details for one aspect of the current + state of this API Resource.\n---\nThis struct is intended for + direct use as an array at the field path .status.conditions. For + example,\n\n\n\ttype FooStatus struct{\n\t // Represents the + observations of a foo's current state.\n\t // Known .status.conditions.type + are: \"Available\", \"Progressing\", and \"Degraded\"\n\t // + +patchMergeKey=type\n\t // +patchStrategy=merge\n\t // +listType=map\n\t + \ // +listMapKey=type\n\t Conditions []metav1.Condition `json:\"conditions,omitempty\" + patchStrategy:\"merge\" patchMergeKey:\"type\" protobuf:\"bytes,1,rep,name=conditions\"`\n\n\n\t + \ // other fields\n\t}" + properties: + lastTransitionTime: + description: |- + lastTransitionTime is the last time the condition transitioned from one status to another. + This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: |- + message is a human readable message indicating details about the transition. + This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: |- + observedGeneration represents the .metadata.generation that the condition was set based upon. + For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date + with respect to the current state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: |- + reason contains a programmatic identifier indicating the reason for the condition's last transition. + Producers of specific condition types may define expected values and meanings for this field, + and whether the values are considered a guaranteed API. + The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: |- + type of condition in CamelCase or in foo.example.com/CamelCase. + --- + Many .condition.type values are consistent across resources like Available, but because arbitrary conditions can be + useful (see .node.status.conditions), the ability to deconflict is important. + The regex it matches is (dns1123SubdomainFmt/)?(qualifiedNameFmt) + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + x-kubernetes-list-map-keys: + - type + x-kubernetes-list-type: map + phase: + default: Pending + description: Phase summarizes the overall status of the GitHubIdentityProvider. + enum: + - Pending + - Ready + - Error + type: string + type: object + required: + - spec + type: object + served: true + storage: true + subresources: + status: {} diff --git a/generated/1.24/crds/idp.supervisor.pinniped.dev_ldapidentityproviders.yaml b/generated/1.24/crds/idp.supervisor.pinniped.dev_ldapidentityproviders.yaml index f718e93aa..711e9a754 100644 --- a/generated/1.24/crds/idp.supervisor.pinniped.dev_ldapidentityproviders.yaml +++ b/generated/1.24/crds/idp.supervisor.pinniped.dev_ldapidentityproviders.yaml @@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - controller-gen.kubebuilder.io/version: v0.14.0 + controller-gen.kubebuilder.io/version: v0.15.0 name: ldapidentityproviders.idp.supervisor.pinniped.dev spec: group: idp.supervisor.pinniped.dev diff --git a/generated/1.24/crds/idp.supervisor.pinniped.dev_oidcidentityproviders.yaml b/generated/1.24/crds/idp.supervisor.pinniped.dev_oidcidentityproviders.yaml index 486665605..acfca1573 100644 --- a/generated/1.24/crds/idp.supervisor.pinniped.dev_oidcidentityproviders.yaml +++ b/generated/1.24/crds/idp.supervisor.pinniped.dev_oidcidentityproviders.yaml @@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - controller-gen.kubebuilder.io/version: v0.14.0 + controller-gen.kubebuilder.io/version: v0.15.0 name: oidcidentityproviders.idp.supervisor.pinniped.dev spec: group: idp.supervisor.pinniped.dev diff --git a/generated/1.25/README.adoc b/generated/1.25/README.adoc index 843304237..3ae558150 100644 --- a/generated/1.25/README.adoc +++ b/generated/1.25/README.adoc @@ -1645,6 +1645,285 @@ Optional, when empty this defaults to "objectGUID". + |=== +[id="{anchor_prefix}-go-pinniped-dev-generated-1-25-apis-supervisor-idp-v1alpha1-githubapiconfig"] +==== GitHubAPIConfig + +GitHubAPIConfig allows configuration for GitHub Enterprise Server + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-25-apis-supervisor-idp-v1alpha1-githubidentityproviderspec[$$GitHubIdentityProviderSpec$$] +**** + +[cols="25a,75a", options="header"] +|=== +| Field | Description +| *`host`* __string__ | Host is required only for GitHub Enterprise Server. + +Defaults to using GitHub's public API ("github.com"). + +Do not specify a protocol or scheme since "https://" will always be used. + +Port is optional. Do not specify a path, query, fragment, or userinfo. + +Only domain name or IP address, subdomains (optional), and port (optional). + +IPv4 and IPv6 are supported. If using an IPv6 address with a port, you must enclose the IPv6 address + +in square brackets. Example: "[::1]:443". + +| *`tls`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-25-apis-supervisor-idp-v1alpha1-tlsspec[$$TLSSpec$$]__ | TLS configuration for GitHub Enterprise Server. + +|=== + + +[id="{anchor_prefix}-go-pinniped-dev-generated-1-25-apis-supervisor-idp-v1alpha1-githuballowauthenticationspec"] +==== GitHubAllowAuthenticationSpec + +GitHubAllowAuthenticationSpec allows customization of who can authenticate using this IDP and how. + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-25-apis-supervisor-idp-v1alpha1-githubidentityproviderspec[$$GitHubIdentityProviderSpec$$] +**** + +[cols="25a,75a", options="header"] +|=== +| Field | Description +| *`organizations`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-25-apis-supervisor-idp-v1alpha1-githuborganizationsspec[$$GitHubOrganizationsSpec$$]__ | Organizations allows customization of which organizations can authenticate using this IDP. + +|=== + + +[id="{anchor_prefix}-go-pinniped-dev-generated-1-25-apis-supervisor-idp-v1alpha1-githuballowedauthorganizationspolicy"] +==== GitHubAllowedAuthOrganizationsPolicy (string) + + + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-25-apis-supervisor-idp-v1alpha1-githuborganizationsspec[$$GitHubOrganizationsSpec$$] +**** + + + +[id="{anchor_prefix}-go-pinniped-dev-generated-1-25-apis-supervisor-idp-v1alpha1-githubclaims"] +==== GitHubClaims + +GitHubClaims allows customization of the username and groups claims. + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-25-apis-supervisor-idp-v1alpha1-githubidentityproviderspec[$$GitHubIdentityProviderSpec$$] +**** + +[cols="25a,75a", options="header"] +|=== +| Field | Description +| *`username`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-25-apis-supervisor-idp-v1alpha1-githubusernameattribute[$$GitHubUsernameAttribute$$]__ | Username configures which property of the GitHub user record shall determine the username in Kubernetes. + + + +Can be either "id", "login", or "login:id". Defaults to "login:id". + + + +GitHub's user login attributes can only contain alphanumeric characters and non-repeating hyphens, + +and may not start or end with hyphens. GitHub users are allowed to change their login name, + +although it is inconvenient. If a GitHub user changed their login name from "foo" to "bar", + +then a second user might change their name from "baz" to "foo" in order to take the old + +username of the first user. For this reason, it is not as safe to make authorization decisions + +based only on the user's login attribute. + + + +If desired, an admin could configure identity transformation expressions on the Pinniped Supervisor's + +FederationDomain to further customize how these usernames are presented to Kubernetes. + + + +Defaults to "login:id", which is the user login attribute, followed by a colon, followed by the unique and + +unchanging integer ID number attribute. This blends human-readable login names with the unchanging ID value + +from GitHub. Colons are not allowed in GitHub login attributes or ID numbers, so this is a reasonable + +choice to concatenate the two values. + + + +See the response schema for + +[Get the authenticated user](https://docs.github.com/en/rest/users/users?apiVersion=2022-11-28#get-the-authenticated-user). + +| *`groups`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-25-apis-supervisor-idp-v1alpha1-githubgroupnameattribute[$$GitHubGroupNameAttribute$$]__ | Groups configures which property of the GitHub team record shall determine the group names in Kubernetes. + + + +Can be either "name" or "slug". Defaults to "slug". + + + +GitHub team names can contain upper and lower case characters, whitespace, and punctuation (e.g. "Kube admins!"). + + + +GitHub team slugs are lower case alphanumeric characters and may contain dashes and underscores (e.g. "kube-admins"). + + + +Group names as presented to Kubernetes will always be prefixed by the GitHub organization name followed by a + +forward slash (e.g. "my-org/my-team"). GitHub organization login names can only contain alphanumeric characters + +or single hyphens, so the first forward slash `/` will be the separator between the organization login name and + +the team name or slug. + + + +If desired, an admin could configure identity transformation expressions on the Pinniped Supervisor's + +FederationDomain to further customize how these group names are presented to Kubernetes. + + + +See the response schema for + +[List teams for the authenticated user](https://docs.github.com/en/rest/teams/teams?apiVersion=2022-11-28#list-teams-for-the-authenticated-user). + +|=== + + +[id="{anchor_prefix}-go-pinniped-dev-generated-1-25-apis-supervisor-idp-v1alpha1-githubclientspec"] +==== GitHubClientSpec + +GitHubClientSpec contains information about the GitHub client that this identity provider will use +for web-based login flows. + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-25-apis-supervisor-idp-v1alpha1-githubidentityproviderspec[$$GitHubIdentityProviderSpec$$] +**** + +[cols="25a,75a", options="header"] +|=== +| Field | Description +| *`secretName`* __string__ | SecretName contains the name of a namespace-local Secret object that provides the clientID and + +clientSecret for an GitHub App or GitHub OAuth2 client. + + + +This secret must be of type "secrets.pinniped.dev/github-client" with keys "clientID" and "clientSecret". + +|=== + + +[id="{anchor_prefix}-go-pinniped-dev-generated-1-25-apis-supervisor-idp-v1alpha1-githubgroupnameattribute"] +==== GitHubGroupNameAttribute (string) + +GitHubGroupNameAttribute allows the user to specify which attribute from GitHub to use for the group +names to present to Kubernetes. See the response schema for +[List teams for the authenticated user](https://docs.github.com/en/rest/teams/teams?apiVersion=2022-11-28#list-teams-for-the-authenticated-user). + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-25-apis-supervisor-idp-v1alpha1-githubclaims[$$GitHubClaims$$] +**** + + + +[id="{anchor_prefix}-go-pinniped-dev-generated-1-25-apis-supervisor-idp-v1alpha1-githubidentityprovider"] +==== GitHubIdentityProvider + +GitHubIdentityProvider describes the configuration of an upstream GitHub identity provider. +This upstream provider can be configured with either a GitHub App or a GitHub OAuth2 App. + + +Right now, only web-based logins are supported, for both the pinniped-cli client and clients configured +as OIDCClients. + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-25-apis-supervisor-idp-v1alpha1-githubidentityproviderlist[$$GitHubIdentityProviderList$$] +**** + +[cols="25a,75a", options="header"] +|=== +| Field | Description +| *`metadata`* __link:https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.25/#objectmeta-v1-meta[$$ObjectMeta$$]__ | Refer to Kubernetes API documentation for fields of `metadata`. + +| *`spec`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-25-apis-supervisor-idp-v1alpha1-githubidentityproviderspec[$$GitHubIdentityProviderSpec$$]__ | Spec for configuring the identity provider. + +| *`status`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-25-apis-supervisor-idp-v1alpha1-githubidentityproviderstatus[$$GitHubIdentityProviderStatus$$]__ | Status of the identity provider. + +|=== + + + + +[id="{anchor_prefix}-go-pinniped-dev-generated-1-25-apis-supervisor-idp-v1alpha1-githubidentityproviderphase"] +==== GitHubIdentityProviderPhase (string) + + + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-25-apis-supervisor-idp-v1alpha1-githubidentityproviderstatus[$$GitHubIdentityProviderStatus$$] +**** + + + +[id="{anchor_prefix}-go-pinniped-dev-generated-1-25-apis-supervisor-idp-v1alpha1-githubidentityproviderspec"] +==== GitHubIdentityProviderSpec + +GitHubIdentityProviderSpec is the spec for configuring an GitHub identity provider. + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-25-apis-supervisor-idp-v1alpha1-githubidentityprovider[$$GitHubIdentityProvider$$] +**** + +[cols="25a,75a", options="header"] +|=== +| Field | Description +| *`githubAPI`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-25-apis-supervisor-idp-v1alpha1-githubapiconfig[$$GitHubAPIConfig$$]__ | GitHubAPI allows configuration for GitHub Enterprise Server + +| *`claims`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-25-apis-supervisor-idp-v1alpha1-githubclaims[$$GitHubClaims$$]__ | Claims allows customization of the username and groups claims. + +| *`allowAuthentication`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-25-apis-supervisor-idp-v1alpha1-githuballowauthenticationspec[$$GitHubAllowAuthenticationSpec$$]__ | AllowAuthentication allows customization of who can authenticate using this IDP and how. + +| *`client`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-25-apis-supervisor-idp-v1alpha1-githubclientspec[$$GitHubClientSpec$$]__ | Client identifies the secret with credentials for a GitHub App or GitHub OAuth2 App (a GitHub client). + +|=== + + +[id="{anchor_prefix}-go-pinniped-dev-generated-1-25-apis-supervisor-idp-v1alpha1-githubidentityproviderstatus"] +==== GitHubIdentityProviderStatus + +GitHubIdentityProviderStatus is the status of an GitHub identity provider. + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-25-apis-supervisor-idp-v1alpha1-githubidentityprovider[$$GitHubIdentityProvider$$] +**** + +[cols="25a,75a", options="header"] +|=== +| Field | Description +| *`phase`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-25-apis-supervisor-idp-v1alpha1-githubidentityproviderphase[$$GitHubIdentityProviderPhase$$]__ | Phase summarizes the overall status of the GitHubIdentityProvider. + +| *`conditions`* __link:https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.25/#condition-v1-meta[$$Condition$$] array__ | Conditions represents the observations of an identity provider's current state. + +|=== + + +[id="{anchor_prefix}-go-pinniped-dev-generated-1-25-apis-supervisor-idp-v1alpha1-githuborganizationsspec"] +==== GitHubOrganizationsSpec + + + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-25-apis-supervisor-idp-v1alpha1-githuballowauthenticationspec[$$GitHubAllowAuthenticationSpec$$] +**** + +[cols="25a,75a", options="header"] +|=== +| Field | Description +| *`policy`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-25-apis-supervisor-idp-v1alpha1-githuballowedauthorganizationspolicy[$$GitHubAllowedAuthOrganizationsPolicy$$]__ | Policy must be set to "AllGitHubUsers" if allowed is empty. + + + +This field only exists to ensure that Pinniped administrators are aware that an empty list of + +allowedOrganizations means all GitHub users are allowed to log in. + +| *`allowed`* __string array__ | Allowed, when specified, indicates that only users with membership in at least one of the listed + +GitHub organizations may log in. In addition, the group membership presented to Kubernetes will only include + +teams within the listed GitHub organizations. Additional login rules or group filtering can optionally be + +provided as policy expression on any Pinniped Supervisor FederationDomain that includes this IDP. + + + +The configured GitHub App or GitHub OAuth App must be allowed to see membership in the listed organizations, + +otherwise Pinniped will not be aware that the user belongs to the listed organization or any teams + +within that organization. + + + +If no organizations are listed, you must set organizations: AllGitHubUsers. + +|=== + + +[id="{anchor_prefix}-go-pinniped-dev-generated-1-25-apis-supervisor-idp-v1alpha1-githubusernameattribute"] +==== GitHubUsernameAttribute (string) + +GitHubUsernameAttribute allows the user to specify which attribute(s) from GitHub to use for the username to present +to Kubernetes. See the response schema for +[Get the authenticated user](https://docs.github.com/en/rest/users/users?apiVersion=2022-11-28#get-the-authenticated-user). + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-25-apis-supervisor-idp-v1alpha1-githubclaims[$$GitHubClaims$$] +**** + + + [id="{anchor_prefix}-go-pinniped-dev-generated-1-25-apis-supervisor-idp-v1alpha1-ldapidentityprovider"] ==== LDAPIdentityProvider @@ -2108,11 +2387,12 @@ Parameter is a key/value pair which represents a parameter in an HTTP request. [id="{anchor_prefix}-go-pinniped-dev-generated-1-25-apis-supervisor-idp-v1alpha1-tlsspec"] ==== TLSSpec -Configuration for TLS parameters related to identity provider integration. +TLSSpec provides TLS configuration for identity provider integration. .Appears In: **** - xref:{anchor_prefix}-go-pinniped-dev-generated-1-25-apis-supervisor-idp-v1alpha1-activedirectoryidentityproviderspec[$$ActiveDirectoryIdentityProviderSpec$$] +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-25-apis-supervisor-idp-v1alpha1-githubapiconfig[$$GitHubAPIConfig$$] - xref:{anchor_prefix}-go-pinniped-dev-generated-1-25-apis-supervisor-idp-v1alpha1-ldapidentityproviderspec[$$LDAPIdentityProviderSpec$$] - xref:{anchor_prefix}-go-pinniped-dev-generated-1-25-apis-supervisor-idp-v1alpha1-oidcidentityproviderspec[$$OIDCIdentityProviderSpec$$] **** diff --git a/generated/1.25/apis/supervisor/idp/v1alpha1/register.go b/generated/1.25/apis/supervisor/idp/v1alpha1/register.go index 8829a8638..705be8076 100644 --- a/generated/1.25/apis/supervisor/idp/v1alpha1/register.go +++ b/generated/1.25/apis/supervisor/idp/v1alpha1/register.go @@ -1,4 +1,4 @@ -// Copyright 2020-2022 the Pinniped contributors. All Rights Reserved. +// Copyright 2020-2024 the Pinniped contributors. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 package v1alpha1 @@ -36,6 +36,8 @@ func addKnownTypes(scheme *runtime.Scheme) error { &LDAPIdentityProviderList{}, &ActiveDirectoryIdentityProvider{}, &ActiveDirectoryIdentityProviderList{}, + &GitHubIdentityProvider{}, + &GitHubIdentityProviderList{}, ) metav1.AddToGroupVersion(scheme, SchemeGroupVersion) return nil diff --git a/generated/1.25/apis/supervisor/idp/v1alpha1/types_githubidentityprovider.go b/generated/1.25/apis/supervisor/idp/v1alpha1/types_githubidentityprovider.go new file mode 100644 index 000000000..c84f46dbd --- /dev/null +++ b/generated/1.25/apis/supervisor/idp/v1alpha1/types_githubidentityprovider.go @@ -0,0 +1,256 @@ +// Copyright 2024 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package v1alpha1 + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +type GitHubIdentityProviderPhase string + +const ( + // GitHubPhasePending is the default phase for newly-created GitHubIdentityProvider resources. + GitHubPhasePending GitHubIdentityProviderPhase = "Pending" + + // GitHubPhaseReady is the phase for an GitHubIdentityProvider resource in a healthy state. + GitHubPhaseReady GitHubIdentityProviderPhase = "Ready" + + // GitHubPhaseError is the phase for an GitHubIdentityProvider in an unhealthy state. + GitHubPhaseError GitHubIdentityProviderPhase = "Error" +) + +type GitHubAllowedAuthOrganizationsPolicy string + +const ( + // GitHubAllowedAuthOrganizationsPolicyAllGitHubUsers means any GitHub user is allowed to log in using this identity + // provider, regardless of their organization membership or lack thereof. + GitHubAllowedAuthOrganizationsPolicyAllGitHubUsers GitHubAllowedAuthOrganizationsPolicy = "AllGitHubUsers" + + // GitHubAllowedAuthOrganizationsPolicyOnlyUsersFromAllowedOrganizations means only those users with membership in + // the listed GitHub organizations are allowed to log in. + GitHubAllowedAuthOrganizationsPolicyOnlyUsersFromAllowedOrganizations GitHubAllowedAuthOrganizationsPolicy = "OnlyUsersFromAllowedOrganizations" +) + +// GitHubIdentityProviderStatus is the status of an GitHub identity provider. +type GitHubIdentityProviderStatus struct { + // Phase summarizes the overall status of the GitHubIdentityProvider. + // + // +kubebuilder:default=Pending + // +kubebuilder:validation:Enum=Pending;Ready;Error + Phase GitHubIdentityProviderPhase `json:"phase,omitempty"` + + // Conditions represents the observations of an identity provider's current state. + // + // +patchMergeKey=type + // +patchStrategy=merge + // +listType=map + // +listMapKey=type + Conditions []metav1.Condition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type"` +} + +// GitHubAPIConfig allows configuration for GitHub Enterprise Server +type GitHubAPIConfig struct { + // Host is required only for GitHub Enterprise Server. + // Defaults to using GitHub's public API ("github.com"). + // Do not specify a protocol or scheme since "https://" will always be used. + // Port is optional. Do not specify a path, query, fragment, or userinfo. + // Only domain name or IP address, subdomains (optional), and port (optional). + // IPv4 and IPv6 are supported. If using an IPv6 address with a port, you must enclose the IPv6 address + // in square brackets. Example: "[::1]:443". + // + // +kubebuilder:default="github.com" + // +kubebuilder:validation:MinLength=1 + // +optional + Host *string `json:"host"` + + // TLS configuration for GitHub Enterprise Server. + // + // +optional + TLS *TLSSpec `json:"tls,omitempty"` +} + +// GitHubUsernameAttribute allows the user to specify which attribute(s) from GitHub to use for the username to present +// to Kubernetes. See the response schema for +// [Get the authenticated user](https://docs.github.com/en/rest/users/users?apiVersion=2022-11-28#get-the-authenticated-user). +type GitHubUsernameAttribute string + +const ( + // GitHubUsernameID specifies using the `id` attribute from the GitHub user for the username to present to Kubernetes. + GitHubUsernameID GitHubUsernameAttribute = "id" + + // GitHubUsernameLogin specifies using the `login` attribute from the GitHub user as the username to present to Kubernetes. + GitHubUsernameLogin GitHubUsernameAttribute = "login" + + // GitHubUsernameLoginAndID specifies combining the `login` and `id` attributes from the GitHub user as the + // username to present to Kubernetes, separated by a colon. Example: "my-login:1234" + GitHubUsernameLoginAndID GitHubUsernameAttribute = "login:id" +) + +// GitHubGroupNameAttribute allows the user to specify which attribute from GitHub to use for the group +// names to present to Kubernetes. See the response schema for +// [List teams for the authenticated user](https://docs.github.com/en/rest/teams/teams?apiVersion=2022-11-28#list-teams-for-the-authenticated-user). +type GitHubGroupNameAttribute string + +const ( + // GitHubUseTeamNameForGroupName specifies using the GitHub team's `name` attribute as the group name to present to Kubernetes. + GitHubUseTeamNameForGroupName GitHubGroupNameAttribute = "name" + + // GitHubUseTeamSlugForGroupName specifies using the GitHub team's `slug` attribute as the group name to present to Kubernetes. + GitHubUseTeamSlugForGroupName GitHubGroupNameAttribute = "slug" +) + +// GitHubClaims allows customization of the username and groups claims. +type GitHubClaims struct { + // Username configures which property of the GitHub user record shall determine the username in Kubernetes. + // + // Can be either "id", "login", or "login:id". Defaults to "login:id". + // + // GitHub's user login attributes can only contain alphanumeric characters and non-repeating hyphens, + // and may not start or end with hyphens. GitHub users are allowed to change their login name, + // although it is inconvenient. If a GitHub user changed their login name from "foo" to "bar", + // then a second user might change their name from "baz" to "foo" in order to take the old + // username of the first user. For this reason, it is not as safe to make authorization decisions + // based only on the user's login attribute. + // + // If desired, an admin could configure identity transformation expressions on the Pinniped Supervisor's + // FederationDomain to further customize how these usernames are presented to Kubernetes. + // + // Defaults to "login:id", which is the user login attribute, followed by a colon, followed by the unique and + // unchanging integer ID number attribute. This blends human-readable login names with the unchanging ID value + // from GitHub. Colons are not allowed in GitHub login attributes or ID numbers, so this is a reasonable + // choice to concatenate the two values. + // + // See the response schema for + // [Get the authenticated user](https://docs.github.com/en/rest/users/users?apiVersion=2022-11-28#get-the-authenticated-user). + // + // +kubebuilder:default="login:id" + // +kubebuilder:validation:Enum={"id","login","login:id"} + // +optional + Username *GitHubUsernameAttribute `json:"username"` + + // Groups configures which property of the GitHub team record shall determine the group names in Kubernetes. + // + // Can be either "name" or "slug". Defaults to "slug". + // + // GitHub team names can contain upper and lower case characters, whitespace, and punctuation (e.g. "Kube admins!"). + // + // GitHub team slugs are lower case alphanumeric characters and may contain dashes and underscores (e.g. "kube-admins"). + // + // Group names as presented to Kubernetes will always be prefixed by the GitHub organization name followed by a + // forward slash (e.g. "my-org/my-team"). GitHub organization login names can only contain alphanumeric characters + // or single hyphens, so the first forward slash `/` will be the separator between the organization login name and + // the team name or slug. + // + // If desired, an admin could configure identity transformation expressions on the Pinniped Supervisor's + // FederationDomain to further customize how these group names are presented to Kubernetes. + // + // See the response schema for + // [List teams for the authenticated user](https://docs.github.com/en/rest/teams/teams?apiVersion=2022-11-28#list-teams-for-the-authenticated-user). + // + // +kubebuilder:default=slug + // +kubebuilder:validation:Enum=name;slug + // +optional + Groups *GitHubGroupNameAttribute `json:"groups"` +} + +// GitHubClientSpec contains information about the GitHub client that this identity provider will use +// for web-based login flows. +type GitHubClientSpec struct { + // SecretName contains the name of a namespace-local Secret object that provides the clientID and + // clientSecret for an GitHub App or GitHub OAuth2 client. + // + // This secret must be of type "secrets.pinniped.dev/github-client" with keys "clientID" and "clientSecret". + // + // +kubebuilder:validation:MinLength=1 + SecretName string `json:"secretName"` +} + +type GitHubOrganizationsSpec struct { + // Policy must be set to "AllGitHubUsers" if allowed is empty. + // + // This field only exists to ensure that Pinniped administrators are aware that an empty list of + // allowedOrganizations means all GitHub users are allowed to log in. + // + // +kubebuilder:default=OnlyUsersFromAllowedOrganizations + // +kubebuilder:validation:Enum=OnlyUsersFromAllowedOrganizations;AllGitHubUsers + // +optional + Policy *GitHubAllowedAuthOrganizationsPolicy `json:"policy"` + + // Allowed, when specified, indicates that only users with membership in at least one of the listed + // GitHub organizations may log in. In addition, the group membership presented to Kubernetes will only include + // teams within the listed GitHub organizations. Additional login rules or group filtering can optionally be + // provided as policy expression on any Pinniped Supervisor FederationDomain that includes this IDP. + // + // The configured GitHub App or GitHub OAuth App must be allowed to see membership in the listed organizations, + // otherwise Pinniped will not be aware that the user belongs to the listed organization or any teams + // within that organization. + // + // If no organizations are listed, you must set organizations: AllGitHubUsers. + // + // +kubebuilder:validation:MaxItems=64 + // +listType=set + // +optional + Allowed []string `json:"allowed,omitempty"` +} + +// GitHubAllowAuthenticationSpec allows customization of who can authenticate using this IDP and how. +type GitHubAllowAuthenticationSpec struct { + // Organizations allows customization of which organizations can authenticate using this IDP. + // +kubebuilder:validation:XValidation:message="spec.allowAuthentication.organizations.policy must be 'OnlyUsersFromAllowedOrganizations' when spec.allowAuthentication.organizations.allowed has organizations listed",rule="!(has(self.allowed) && size(self.allowed) > 0 && self.policy == 'AllGitHubUsers')" + // +kubebuilder:validation:XValidation:message="spec.allowAuthentication.organizations.policy must be 'AllGitHubUsers' when spec.allowAuthentication.organizations.allowed is empty",rule="!((!has(self.allowed) || size(self.allowed) == 0) && self.policy == 'OnlyUsersFromAllowedOrganizations')" + Organizations GitHubOrganizationsSpec `json:"organizations"` +} + +// GitHubIdentityProviderSpec is the spec for configuring an GitHub identity provider. +type GitHubIdentityProviderSpec struct { + // GitHubAPI allows configuration for GitHub Enterprise Server + // + // +kubebuilder:default={} + GitHubAPI GitHubAPIConfig `json:"githubAPI,omitempty"` + + // Claims allows customization of the username and groups claims. + // + // +kubebuilder:default={} + Claims GitHubClaims `json:"claims,omitempty"` + + // AllowAuthentication allows customization of who can authenticate using this IDP and how. + AllowAuthentication GitHubAllowAuthenticationSpec `json:"allowAuthentication"` + + // Client identifies the secret with credentials for a GitHub App or GitHub OAuth2 App (a GitHub client). + Client GitHubClientSpec `json:"client"` +} + +// GitHubIdentityProvider describes the configuration of an upstream GitHub identity provider. +// This upstream provider can be configured with either a GitHub App or a GitHub OAuth2 App. +// +// Right now, only web-based logins are supported, for both the pinniped-cli client and clients configured +// as OIDCClients. +// +// +genclient +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object +// +kubebuilder:resource:categories=pinniped;pinniped-idp;pinniped-idps +// +kubebuilder:printcolumn:name="Host",type=string,JSONPath=`.spec.githubAPI.host` +// +kubebuilder:printcolumn:name="Status",type=string,JSONPath=`.status.phase` +// +kubebuilder:printcolumn:name="Age",type=date,JSONPath=`.metadata.creationTimestamp` +// +kubebuilder:subresource:status +type GitHubIdentityProvider struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + // Spec for configuring the identity provider. + Spec GitHubIdentityProviderSpec `json:"spec"` + + // Status of the identity provider. + Status GitHubIdentityProviderStatus `json:"status,omitempty"` +} + +// GitHubIdentityProviderList lists GitHubIdentityProvider objects. +// +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object +type GitHubIdentityProviderList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + + Items []GitHubIdentityProvider `json:"items"` +} diff --git a/generated/1.25/apis/supervisor/idp/v1alpha1/types_tls.go b/generated/1.25/apis/supervisor/idp/v1alpha1/types_tls.go index 1413a262c..49b49373c 100644 --- a/generated/1.25/apis/supervisor/idp/v1alpha1/types_tls.go +++ b/generated/1.25/apis/supervisor/idp/v1alpha1/types_tls.go @@ -1,9 +1,9 @@ -// Copyright 2020-2022 the Pinniped contributors. All Rights Reserved. +// Copyright 2020-2024 the Pinniped contributors. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 package v1alpha1 -// Configuration for TLS parameters related to identity provider integration. +// TLSSpec provides TLS configuration for identity provider integration. type TLSSpec struct { // X.509 Certificate Authority (base64-encoded PEM bundle). If omitted, a default set of system roots will be trusted. // +optional diff --git a/generated/1.25/apis/supervisor/idp/v1alpha1/zz_generated.deepcopy.go b/generated/1.25/apis/supervisor/idp/v1alpha1/zz_generated.deepcopy.go index b0cb168b5..e48860e82 100644 --- a/generated/1.25/apis/supervisor/idp/v1alpha1/zz_generated.deepcopy.go +++ b/generated/1.25/apis/supervisor/idp/v1alpha1/zz_generated.deepcopy.go @@ -203,6 +203,221 @@ func (in *ActiveDirectoryIdentityProviderUserSearchAttributes) DeepCopy() *Activ return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *GitHubAPIConfig) DeepCopyInto(out *GitHubAPIConfig) { + *out = *in + if in.Host != nil { + in, out := &in.Host, &out.Host + *out = new(string) + **out = **in + } + if in.TLS != nil { + in, out := &in.TLS, &out.TLS + *out = new(TLSSpec) + **out = **in + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GitHubAPIConfig. +func (in *GitHubAPIConfig) DeepCopy() *GitHubAPIConfig { + if in == nil { + return nil + } + out := new(GitHubAPIConfig) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *GitHubAllowAuthenticationSpec) DeepCopyInto(out *GitHubAllowAuthenticationSpec) { + *out = *in + in.Organizations.DeepCopyInto(&out.Organizations) + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GitHubAllowAuthenticationSpec. +func (in *GitHubAllowAuthenticationSpec) DeepCopy() *GitHubAllowAuthenticationSpec { + if in == nil { + return nil + } + out := new(GitHubAllowAuthenticationSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *GitHubClaims) DeepCopyInto(out *GitHubClaims) { + *out = *in + if in.Username != nil { + in, out := &in.Username, &out.Username + *out = new(GitHubUsernameAttribute) + **out = **in + } + if in.Groups != nil { + in, out := &in.Groups, &out.Groups + *out = new(GitHubGroupNameAttribute) + **out = **in + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GitHubClaims. +func (in *GitHubClaims) DeepCopy() *GitHubClaims { + if in == nil { + return nil + } + out := new(GitHubClaims) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *GitHubClientSpec) DeepCopyInto(out *GitHubClientSpec) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GitHubClientSpec. +func (in *GitHubClientSpec) DeepCopy() *GitHubClientSpec { + if in == nil { + return nil + } + out := new(GitHubClientSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *GitHubIdentityProvider) DeepCopyInto(out *GitHubIdentityProvider) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + in.Status.DeepCopyInto(&out.Status) + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GitHubIdentityProvider. +func (in *GitHubIdentityProvider) DeepCopy() *GitHubIdentityProvider { + if in == nil { + return nil + } + out := new(GitHubIdentityProvider) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *GitHubIdentityProvider) 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 *GitHubIdentityProviderList) DeepCopyInto(out *GitHubIdentityProviderList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]GitHubIdentityProvider, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GitHubIdentityProviderList. +func (in *GitHubIdentityProviderList) DeepCopy() *GitHubIdentityProviderList { + if in == nil { + return nil + } + out := new(GitHubIdentityProviderList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *GitHubIdentityProviderList) 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 *GitHubIdentityProviderSpec) DeepCopyInto(out *GitHubIdentityProviderSpec) { + *out = *in + in.GitHubAPI.DeepCopyInto(&out.GitHubAPI) + in.Claims.DeepCopyInto(&out.Claims) + in.AllowAuthentication.DeepCopyInto(&out.AllowAuthentication) + out.Client = in.Client + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GitHubIdentityProviderSpec. +func (in *GitHubIdentityProviderSpec) DeepCopy() *GitHubIdentityProviderSpec { + if in == nil { + return nil + } + out := new(GitHubIdentityProviderSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *GitHubIdentityProviderStatus) DeepCopyInto(out *GitHubIdentityProviderStatus) { + *out = *in + if in.Conditions != nil { + in, out := &in.Conditions, &out.Conditions + *out = make([]v1.Condition, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GitHubIdentityProviderStatus. +func (in *GitHubIdentityProviderStatus) DeepCopy() *GitHubIdentityProviderStatus { + if in == nil { + return nil + } + out := new(GitHubIdentityProviderStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *GitHubOrganizationsSpec) DeepCopyInto(out *GitHubOrganizationsSpec) { + *out = *in + if in.Policy != nil { + in, out := &in.Policy, &out.Policy + *out = new(GitHubAllowedAuthOrganizationsPolicy) + **out = **in + } + if in.Allowed != nil { + in, out := &in.Allowed, &out.Allowed + *out = make([]string, len(*in)) + copy(*out, *in) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GitHubOrganizationsSpec. +func (in *GitHubOrganizationsSpec) DeepCopy() *GitHubOrganizationsSpec { + if in == nil { + return nil + } + out := new(GitHubOrganizationsSpec) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *LDAPIdentityProvider) DeepCopyInto(out *LDAPIdentityProvider) { *out = *in diff --git a/generated/1.25/apis/supervisor/idpdiscovery/v1alpha1/types_supervisor_idp_discovery.go b/generated/1.25/apis/supervisor/idpdiscovery/v1alpha1/types_supervisor_idp_discovery.go index 837fed2dd..11be6f9dc 100644 --- a/generated/1.25/apis/supervisor/idpdiscovery/v1alpha1/types_supervisor_idp_discovery.go +++ b/generated/1.25/apis/supervisor/idpdiscovery/v1alpha1/types_supervisor_idp_discovery.go @@ -15,6 +15,7 @@ const ( IDPTypeOIDC IDPType = "oidc" IDPTypeLDAP IDPType = "ldap" IDPTypeActiveDirectory IDPType = "activedirectory" + IDPTypeGitHub IDPType = "github" IDPFlowCLIPassword IDPFlow = "cli_password" IDPFlowBrowserAuthcode IDPFlow = "browser_authcode" diff --git a/generated/1.25/client/supervisor/clientset/versioned/typed/idp/v1alpha1/fake/fake_githubidentityprovider.go b/generated/1.25/client/supervisor/clientset/versioned/typed/idp/v1alpha1/fake/fake_githubidentityprovider.go new file mode 100644 index 000000000..45c58d6c9 --- /dev/null +++ b/generated/1.25/client/supervisor/clientset/versioned/typed/idp/v1alpha1/fake/fake_githubidentityprovider.go @@ -0,0 +1,129 @@ +// Copyright 2020-2024 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +// Code generated by client-gen. DO NOT EDIT. + +package fake + +import ( + "context" + + v1alpha1 "go.pinniped.dev/generated/1.25/apis/supervisor/idp/v1alpha1" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + labels "k8s.io/apimachinery/pkg/labels" + schema "k8s.io/apimachinery/pkg/runtime/schema" + types "k8s.io/apimachinery/pkg/types" + watch "k8s.io/apimachinery/pkg/watch" + testing "k8s.io/client-go/testing" +) + +// FakeGitHubIdentityProviders implements GitHubIdentityProviderInterface +type FakeGitHubIdentityProviders struct { + Fake *FakeIDPV1alpha1 + ns string +} + +var githubidentityprovidersResource = schema.GroupVersionResource{Group: "idp.supervisor.pinniped.dev", Version: "v1alpha1", Resource: "githubidentityproviders"} + +var githubidentityprovidersKind = schema.GroupVersionKind{Group: "idp.supervisor.pinniped.dev", Version: "v1alpha1", Kind: "GitHubIdentityProvider"} + +// Get takes name of the gitHubIdentityProvider, and returns the corresponding gitHubIdentityProvider object, and an error if there is any. +func (c *FakeGitHubIdentityProviders) Get(ctx context.Context, name string, options v1.GetOptions) (result *v1alpha1.GitHubIdentityProvider, err error) { + obj, err := c.Fake. + Invokes(testing.NewGetAction(githubidentityprovidersResource, c.ns, name), &v1alpha1.GitHubIdentityProvider{}) + + if obj == nil { + return nil, err + } + return obj.(*v1alpha1.GitHubIdentityProvider), err +} + +// List takes label and field selectors, and returns the list of GitHubIdentityProviders that match those selectors. +func (c *FakeGitHubIdentityProviders) List(ctx context.Context, opts v1.ListOptions) (result *v1alpha1.GitHubIdentityProviderList, err error) { + obj, err := c.Fake. + Invokes(testing.NewListAction(githubidentityprovidersResource, githubidentityprovidersKind, c.ns, opts), &v1alpha1.GitHubIdentityProviderList{}) + + if obj == nil { + return nil, err + } + + label, _, _ := testing.ExtractFromListOptions(opts) + if label == nil { + label = labels.Everything() + } + list := &v1alpha1.GitHubIdentityProviderList{ListMeta: obj.(*v1alpha1.GitHubIdentityProviderList).ListMeta} + for _, item := range obj.(*v1alpha1.GitHubIdentityProviderList).Items { + if label.Matches(labels.Set(item.Labels)) { + list.Items = append(list.Items, item) + } + } + return list, err +} + +// Watch returns a watch.Interface that watches the requested gitHubIdentityProviders. +func (c *FakeGitHubIdentityProviders) Watch(ctx context.Context, opts v1.ListOptions) (watch.Interface, error) { + return c.Fake. + InvokesWatch(testing.NewWatchAction(githubidentityprovidersResource, c.ns, opts)) + +} + +// Create takes the representation of a gitHubIdentityProvider and creates it. Returns the server's representation of the gitHubIdentityProvider, and an error, if there is any. +func (c *FakeGitHubIdentityProviders) Create(ctx context.Context, gitHubIdentityProvider *v1alpha1.GitHubIdentityProvider, opts v1.CreateOptions) (result *v1alpha1.GitHubIdentityProvider, err error) { + obj, err := c.Fake. + Invokes(testing.NewCreateAction(githubidentityprovidersResource, c.ns, gitHubIdentityProvider), &v1alpha1.GitHubIdentityProvider{}) + + if obj == nil { + return nil, err + } + return obj.(*v1alpha1.GitHubIdentityProvider), err +} + +// Update takes the representation of a gitHubIdentityProvider and updates it. Returns the server's representation of the gitHubIdentityProvider, and an error, if there is any. +func (c *FakeGitHubIdentityProviders) Update(ctx context.Context, gitHubIdentityProvider *v1alpha1.GitHubIdentityProvider, opts v1.UpdateOptions) (result *v1alpha1.GitHubIdentityProvider, err error) { + obj, err := c.Fake. + Invokes(testing.NewUpdateAction(githubidentityprovidersResource, c.ns, gitHubIdentityProvider), &v1alpha1.GitHubIdentityProvider{}) + + if obj == nil { + return nil, err + } + return obj.(*v1alpha1.GitHubIdentityProvider), err +} + +// UpdateStatus was generated because the type contains a Status member. +// Add a +genclient:noStatus comment above the type to avoid generating UpdateStatus(). +func (c *FakeGitHubIdentityProviders) UpdateStatus(ctx context.Context, gitHubIdentityProvider *v1alpha1.GitHubIdentityProvider, opts v1.UpdateOptions) (*v1alpha1.GitHubIdentityProvider, error) { + obj, err := c.Fake. + Invokes(testing.NewUpdateSubresourceAction(githubidentityprovidersResource, "status", c.ns, gitHubIdentityProvider), &v1alpha1.GitHubIdentityProvider{}) + + if obj == nil { + return nil, err + } + return obj.(*v1alpha1.GitHubIdentityProvider), err +} + +// Delete takes name of the gitHubIdentityProvider and deletes it. Returns an error if one occurs. +func (c *FakeGitHubIdentityProviders) Delete(ctx context.Context, name string, opts v1.DeleteOptions) error { + _, err := c.Fake. + Invokes(testing.NewDeleteActionWithOptions(githubidentityprovidersResource, c.ns, name, opts), &v1alpha1.GitHubIdentityProvider{}) + + return err +} + +// DeleteCollection deletes a collection of objects. +func (c *FakeGitHubIdentityProviders) DeleteCollection(ctx context.Context, opts v1.DeleteOptions, listOpts v1.ListOptions) error { + action := testing.NewDeleteCollectionAction(githubidentityprovidersResource, c.ns, listOpts) + + _, err := c.Fake.Invokes(action, &v1alpha1.GitHubIdentityProviderList{}) + return err +} + +// Patch applies the patch and returns the patched gitHubIdentityProvider. +func (c *FakeGitHubIdentityProviders) Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts v1.PatchOptions, subresources ...string) (result *v1alpha1.GitHubIdentityProvider, err error) { + obj, err := c.Fake. + Invokes(testing.NewPatchSubresourceAction(githubidentityprovidersResource, c.ns, name, pt, data, subresources...), &v1alpha1.GitHubIdentityProvider{}) + + if obj == nil { + return nil, err + } + return obj.(*v1alpha1.GitHubIdentityProvider), err +} diff --git a/generated/1.25/client/supervisor/clientset/versioned/typed/idp/v1alpha1/fake/fake_idp_client.go b/generated/1.25/client/supervisor/clientset/versioned/typed/idp/v1alpha1/fake/fake_idp_client.go index 0f1478ec2..b24e48b6b 100644 --- a/generated/1.25/client/supervisor/clientset/versioned/typed/idp/v1alpha1/fake/fake_idp_client.go +++ b/generated/1.25/client/supervisor/clientset/versioned/typed/idp/v1alpha1/fake/fake_idp_client.go @@ -19,6 +19,10 @@ func (c *FakeIDPV1alpha1) ActiveDirectoryIdentityProviders(namespace string) v1a return &FakeActiveDirectoryIdentityProviders{c, namespace} } +func (c *FakeIDPV1alpha1) GitHubIdentityProviders(namespace string) v1alpha1.GitHubIdentityProviderInterface { + return &FakeGitHubIdentityProviders{c, namespace} +} + func (c *FakeIDPV1alpha1) LDAPIdentityProviders(namespace string) v1alpha1.LDAPIdentityProviderInterface { return &FakeLDAPIdentityProviders{c, namespace} } diff --git a/generated/1.25/client/supervisor/clientset/versioned/typed/idp/v1alpha1/generated_expansion.go b/generated/1.25/client/supervisor/clientset/versioned/typed/idp/v1alpha1/generated_expansion.go index 79be098d6..a7acc8120 100644 --- a/generated/1.25/client/supervisor/clientset/versioned/typed/idp/v1alpha1/generated_expansion.go +++ b/generated/1.25/client/supervisor/clientset/versioned/typed/idp/v1alpha1/generated_expansion.go @@ -7,6 +7,8 @@ package v1alpha1 type ActiveDirectoryIdentityProviderExpansion interface{} +type GitHubIdentityProviderExpansion interface{} + type LDAPIdentityProviderExpansion interface{} type OIDCIdentityProviderExpansion interface{} diff --git a/generated/1.25/client/supervisor/clientset/versioned/typed/idp/v1alpha1/githubidentityprovider.go b/generated/1.25/client/supervisor/clientset/versioned/typed/idp/v1alpha1/githubidentityprovider.go new file mode 100644 index 000000000..38f338582 --- /dev/null +++ b/generated/1.25/client/supervisor/clientset/versioned/typed/idp/v1alpha1/githubidentityprovider.go @@ -0,0 +1,182 @@ +// Copyright 2020-2024 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +// Code generated by client-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + "context" + "time" + + v1alpha1 "go.pinniped.dev/generated/1.25/apis/supervisor/idp/v1alpha1" + scheme "go.pinniped.dev/generated/1.25/client/supervisor/clientset/versioned/scheme" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + types "k8s.io/apimachinery/pkg/types" + watch "k8s.io/apimachinery/pkg/watch" + rest "k8s.io/client-go/rest" +) + +// GitHubIdentityProvidersGetter has a method to return a GitHubIdentityProviderInterface. +// A group's client should implement this interface. +type GitHubIdentityProvidersGetter interface { + GitHubIdentityProviders(namespace string) GitHubIdentityProviderInterface +} + +// GitHubIdentityProviderInterface has methods to work with GitHubIdentityProvider resources. +type GitHubIdentityProviderInterface interface { + Create(ctx context.Context, gitHubIdentityProvider *v1alpha1.GitHubIdentityProvider, opts v1.CreateOptions) (*v1alpha1.GitHubIdentityProvider, error) + Update(ctx context.Context, gitHubIdentityProvider *v1alpha1.GitHubIdentityProvider, opts v1.UpdateOptions) (*v1alpha1.GitHubIdentityProvider, error) + UpdateStatus(ctx context.Context, gitHubIdentityProvider *v1alpha1.GitHubIdentityProvider, opts v1.UpdateOptions) (*v1alpha1.GitHubIdentityProvider, 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) (*v1alpha1.GitHubIdentityProvider, error) + List(ctx context.Context, opts v1.ListOptions) (*v1alpha1.GitHubIdentityProviderList, 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 *v1alpha1.GitHubIdentityProvider, err error) + GitHubIdentityProviderExpansion +} + +// gitHubIdentityProviders implements GitHubIdentityProviderInterface +type gitHubIdentityProviders struct { + client rest.Interface + ns string +} + +// newGitHubIdentityProviders returns a GitHubIdentityProviders +func newGitHubIdentityProviders(c *IDPV1alpha1Client, namespace string) *gitHubIdentityProviders { + return &gitHubIdentityProviders{ + client: c.RESTClient(), + ns: namespace, + } +} + +// Get takes name of the gitHubIdentityProvider, and returns the corresponding gitHubIdentityProvider object, and an error if there is any. +func (c *gitHubIdentityProviders) Get(ctx context.Context, name string, options v1.GetOptions) (result *v1alpha1.GitHubIdentityProvider, err error) { + result = &v1alpha1.GitHubIdentityProvider{} + err = c.client.Get(). + Namespace(c.ns). + Resource("githubidentityproviders"). + Name(name). + VersionedParams(&options, scheme.ParameterCodec). + Do(ctx). + Into(result) + return +} + +// List takes label and field selectors, and returns the list of GitHubIdentityProviders that match those selectors. +func (c *gitHubIdentityProviders) List(ctx context.Context, opts v1.ListOptions) (result *v1alpha1.GitHubIdentityProviderList, err error) { + var timeout time.Duration + if opts.TimeoutSeconds != nil { + timeout = time.Duration(*opts.TimeoutSeconds) * time.Second + } + result = &v1alpha1.GitHubIdentityProviderList{} + err = c.client.Get(). + Namespace(c.ns). + Resource("githubidentityproviders"). + VersionedParams(&opts, scheme.ParameterCodec). + Timeout(timeout). + Do(ctx). + Into(result) + return +} + +// Watch returns a watch.Interface that watches the requested gitHubIdentityProviders. +func (c *gitHubIdentityProviders) Watch(ctx context.Context, opts v1.ListOptions) (watch.Interface, error) { + var timeout time.Duration + if opts.TimeoutSeconds != nil { + timeout = time.Duration(*opts.TimeoutSeconds) * time.Second + } + opts.Watch = true + return c.client.Get(). + Namespace(c.ns). + Resource("githubidentityproviders"). + VersionedParams(&opts, scheme.ParameterCodec). + Timeout(timeout). + Watch(ctx) +} + +// Create takes the representation of a gitHubIdentityProvider and creates it. Returns the server's representation of the gitHubIdentityProvider, and an error, if there is any. +func (c *gitHubIdentityProviders) Create(ctx context.Context, gitHubIdentityProvider *v1alpha1.GitHubIdentityProvider, opts v1.CreateOptions) (result *v1alpha1.GitHubIdentityProvider, err error) { + result = &v1alpha1.GitHubIdentityProvider{} + err = c.client.Post(). + Namespace(c.ns). + Resource("githubidentityproviders"). + VersionedParams(&opts, scheme.ParameterCodec). + Body(gitHubIdentityProvider). + Do(ctx). + Into(result) + return +} + +// Update takes the representation of a gitHubIdentityProvider and updates it. Returns the server's representation of the gitHubIdentityProvider, and an error, if there is any. +func (c *gitHubIdentityProviders) Update(ctx context.Context, gitHubIdentityProvider *v1alpha1.GitHubIdentityProvider, opts v1.UpdateOptions) (result *v1alpha1.GitHubIdentityProvider, err error) { + result = &v1alpha1.GitHubIdentityProvider{} + err = c.client.Put(). + Namespace(c.ns). + Resource("githubidentityproviders"). + Name(gitHubIdentityProvider.Name). + VersionedParams(&opts, scheme.ParameterCodec). + Body(gitHubIdentityProvider). + Do(ctx). + Into(result) + return +} + +// UpdateStatus was generated because the type contains a Status member. +// Add a +genclient:noStatus comment above the type to avoid generating UpdateStatus(). +func (c *gitHubIdentityProviders) UpdateStatus(ctx context.Context, gitHubIdentityProvider *v1alpha1.GitHubIdentityProvider, opts v1.UpdateOptions) (result *v1alpha1.GitHubIdentityProvider, err error) { + result = &v1alpha1.GitHubIdentityProvider{} + err = c.client.Put(). + Namespace(c.ns). + Resource("githubidentityproviders"). + Name(gitHubIdentityProvider.Name). + SubResource("status"). + VersionedParams(&opts, scheme.ParameterCodec). + Body(gitHubIdentityProvider). + Do(ctx). + Into(result) + return +} + +// Delete takes name of the gitHubIdentityProvider and deletes it. Returns an error if one occurs. +func (c *gitHubIdentityProviders) Delete(ctx context.Context, name string, opts v1.DeleteOptions) error { + return c.client.Delete(). + Namespace(c.ns). + Resource("githubidentityproviders"). + Name(name). + Body(&opts). + Do(ctx). + Error() +} + +// DeleteCollection deletes a collection of objects. +func (c *gitHubIdentityProviders) DeleteCollection(ctx context.Context, opts v1.DeleteOptions, listOpts v1.ListOptions) error { + var timeout time.Duration + if listOpts.TimeoutSeconds != nil { + timeout = time.Duration(*listOpts.TimeoutSeconds) * time.Second + } + return c.client.Delete(). + Namespace(c.ns). + Resource("githubidentityproviders"). + VersionedParams(&listOpts, scheme.ParameterCodec). + Timeout(timeout). + Body(&opts). + Do(ctx). + Error() +} + +// Patch applies the patch and returns the patched gitHubIdentityProvider. +func (c *gitHubIdentityProviders) Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts v1.PatchOptions, subresources ...string) (result *v1alpha1.GitHubIdentityProvider, err error) { + result = &v1alpha1.GitHubIdentityProvider{} + err = c.client.Patch(pt). + Namespace(c.ns). + Resource("githubidentityproviders"). + Name(name). + SubResource(subresources...). + VersionedParams(&opts, scheme.ParameterCodec). + Body(data). + Do(ctx). + Into(result) + return +} diff --git a/generated/1.25/client/supervisor/clientset/versioned/typed/idp/v1alpha1/idp_client.go b/generated/1.25/client/supervisor/clientset/versioned/typed/idp/v1alpha1/idp_client.go index 0b5b9aa4b..2ac25cfa4 100644 --- a/generated/1.25/client/supervisor/clientset/versioned/typed/idp/v1alpha1/idp_client.go +++ b/generated/1.25/client/supervisor/clientset/versioned/typed/idp/v1alpha1/idp_client.go @@ -16,6 +16,7 @@ import ( type IDPV1alpha1Interface interface { RESTClient() rest.Interface ActiveDirectoryIdentityProvidersGetter + GitHubIdentityProvidersGetter LDAPIdentityProvidersGetter OIDCIdentityProvidersGetter } @@ -29,6 +30,10 @@ func (c *IDPV1alpha1Client) ActiveDirectoryIdentityProviders(namespace string) A return newActiveDirectoryIdentityProviders(c, namespace) } +func (c *IDPV1alpha1Client) GitHubIdentityProviders(namespace string) GitHubIdentityProviderInterface { + return newGitHubIdentityProviders(c, namespace) +} + func (c *IDPV1alpha1Client) LDAPIdentityProviders(namespace string) LDAPIdentityProviderInterface { return newLDAPIdentityProviders(c, namespace) } diff --git a/generated/1.25/client/supervisor/informers/externalversions/generic.go b/generated/1.25/client/supervisor/informers/externalversions/generic.go index 0fed0e6a5..3f465b949 100644 --- a/generated/1.25/client/supervisor/informers/externalversions/generic.go +++ b/generated/1.25/client/supervisor/informers/externalversions/generic.go @@ -49,6 +49,8 @@ func (f *sharedInformerFactory) ForResource(resource schema.GroupVersionResource // Group=idp.supervisor.pinniped.dev, Version=v1alpha1 case idpv1alpha1.SchemeGroupVersion.WithResource("activedirectoryidentityproviders"): return &genericInformer{resource: resource.GroupResource(), informer: f.IDP().V1alpha1().ActiveDirectoryIdentityProviders().Informer()}, nil + case idpv1alpha1.SchemeGroupVersion.WithResource("githubidentityproviders"): + return &genericInformer{resource: resource.GroupResource(), informer: f.IDP().V1alpha1().GitHubIdentityProviders().Informer()}, nil case idpv1alpha1.SchemeGroupVersion.WithResource("ldapidentityproviders"): return &genericInformer{resource: resource.GroupResource(), informer: f.IDP().V1alpha1().LDAPIdentityProviders().Informer()}, nil case idpv1alpha1.SchemeGroupVersion.WithResource("oidcidentityproviders"): diff --git a/generated/1.25/client/supervisor/informers/externalversions/idp/v1alpha1/githubidentityprovider.go b/generated/1.25/client/supervisor/informers/externalversions/idp/v1alpha1/githubidentityprovider.go new file mode 100644 index 000000000..68089ae74 --- /dev/null +++ b/generated/1.25/client/supervisor/informers/externalversions/idp/v1alpha1/githubidentityprovider.go @@ -0,0 +1,77 @@ +// Copyright 2020-2024 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +// Code generated by informer-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + "context" + time "time" + + idpv1alpha1 "go.pinniped.dev/generated/1.25/apis/supervisor/idp/v1alpha1" + versioned "go.pinniped.dev/generated/1.25/client/supervisor/clientset/versioned" + internalinterfaces "go.pinniped.dev/generated/1.25/client/supervisor/informers/externalversions/internalinterfaces" + v1alpha1 "go.pinniped.dev/generated/1.25/client/supervisor/listers/idp/v1alpha1" + 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" +) + +// GitHubIdentityProviderInformer provides access to a shared informer and lister for +// GitHubIdentityProviders. +type GitHubIdentityProviderInformer interface { + Informer() cache.SharedIndexInformer + Lister() v1alpha1.GitHubIdentityProviderLister +} + +type gitHubIdentityProviderInformer struct { + factory internalinterfaces.SharedInformerFactory + tweakListOptions internalinterfaces.TweakListOptionsFunc + namespace string +} + +// NewGitHubIdentityProviderInformer constructs a new informer for GitHubIdentityProvider 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 NewGitHubIdentityProviderInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers) cache.SharedIndexInformer { + return NewFilteredGitHubIdentityProviderInformer(client, namespace, resyncPeriod, indexers, nil) +} + +// NewFilteredGitHubIdentityProviderInformer constructs a new informer for GitHubIdentityProvider 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 NewFilteredGitHubIdentityProviderInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers, tweakListOptions internalinterfaces.TweakListOptionsFunc) cache.SharedIndexInformer { + return cache.NewSharedIndexInformer( + &cache.ListWatch{ + ListFunc: func(options v1.ListOptions) (runtime.Object, error) { + if tweakListOptions != nil { + tweakListOptions(&options) + } + return client.IDPV1alpha1().GitHubIdentityProviders(namespace).List(context.TODO(), options) + }, + WatchFunc: func(options v1.ListOptions) (watch.Interface, error) { + if tweakListOptions != nil { + tweakListOptions(&options) + } + return client.IDPV1alpha1().GitHubIdentityProviders(namespace).Watch(context.TODO(), options) + }, + }, + &idpv1alpha1.GitHubIdentityProvider{}, + resyncPeriod, + indexers, + ) +} + +func (f *gitHubIdentityProviderInformer) defaultInformer(client versioned.Interface, resyncPeriod time.Duration) cache.SharedIndexInformer { + return NewFilteredGitHubIdentityProviderInformer(client, f.namespace, resyncPeriod, cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc}, f.tweakListOptions) +} + +func (f *gitHubIdentityProviderInformer) Informer() cache.SharedIndexInformer { + return f.factory.InformerFor(&idpv1alpha1.GitHubIdentityProvider{}, f.defaultInformer) +} + +func (f *gitHubIdentityProviderInformer) Lister() v1alpha1.GitHubIdentityProviderLister { + return v1alpha1.NewGitHubIdentityProviderLister(f.Informer().GetIndexer()) +} diff --git a/generated/1.25/client/supervisor/informers/externalversions/idp/v1alpha1/interface.go b/generated/1.25/client/supervisor/informers/externalversions/idp/v1alpha1/interface.go index 61c46ea00..f25b7995f 100644 --- a/generated/1.25/client/supervisor/informers/externalversions/idp/v1alpha1/interface.go +++ b/generated/1.25/client/supervisor/informers/externalversions/idp/v1alpha1/interface.go @@ -13,6 +13,8 @@ import ( type Interface interface { // ActiveDirectoryIdentityProviders returns a ActiveDirectoryIdentityProviderInformer. ActiveDirectoryIdentityProviders() ActiveDirectoryIdentityProviderInformer + // GitHubIdentityProviders returns a GitHubIdentityProviderInformer. + GitHubIdentityProviders() GitHubIdentityProviderInformer // LDAPIdentityProviders returns a LDAPIdentityProviderInformer. LDAPIdentityProviders() LDAPIdentityProviderInformer // OIDCIdentityProviders returns a OIDCIdentityProviderInformer. @@ -35,6 +37,11 @@ func (v *version) ActiveDirectoryIdentityProviders() ActiveDirectoryIdentityProv return &activeDirectoryIdentityProviderInformer{factory: v.factory, namespace: v.namespace, tweakListOptions: v.tweakListOptions} } +// GitHubIdentityProviders returns a GitHubIdentityProviderInformer. +func (v *version) GitHubIdentityProviders() GitHubIdentityProviderInformer { + return &gitHubIdentityProviderInformer{factory: v.factory, namespace: v.namespace, tweakListOptions: v.tweakListOptions} +} + // LDAPIdentityProviders returns a LDAPIdentityProviderInformer. func (v *version) LDAPIdentityProviders() LDAPIdentityProviderInformer { return &lDAPIdentityProviderInformer{factory: v.factory, namespace: v.namespace, tweakListOptions: v.tweakListOptions} diff --git a/generated/1.25/client/supervisor/listers/idp/v1alpha1/expansion_generated.go b/generated/1.25/client/supervisor/listers/idp/v1alpha1/expansion_generated.go index b491a9e8d..470c06abb 100644 --- a/generated/1.25/client/supervisor/listers/idp/v1alpha1/expansion_generated.go +++ b/generated/1.25/client/supervisor/listers/idp/v1alpha1/expansion_generated.go @@ -13,6 +13,14 @@ type ActiveDirectoryIdentityProviderListerExpansion interface{} // ActiveDirectoryIdentityProviderNamespaceLister. type ActiveDirectoryIdentityProviderNamespaceListerExpansion interface{} +// GitHubIdentityProviderListerExpansion allows custom methods to be added to +// GitHubIdentityProviderLister. +type GitHubIdentityProviderListerExpansion interface{} + +// GitHubIdentityProviderNamespaceListerExpansion allows custom methods to be added to +// GitHubIdentityProviderNamespaceLister. +type GitHubIdentityProviderNamespaceListerExpansion interface{} + // LDAPIdentityProviderListerExpansion allows custom methods to be added to // LDAPIdentityProviderLister. type LDAPIdentityProviderListerExpansion interface{} diff --git a/generated/1.25/client/supervisor/listers/idp/v1alpha1/githubidentityprovider.go b/generated/1.25/client/supervisor/listers/idp/v1alpha1/githubidentityprovider.go new file mode 100644 index 000000000..080cc4743 --- /dev/null +++ b/generated/1.25/client/supervisor/listers/idp/v1alpha1/githubidentityprovider.go @@ -0,0 +1,86 @@ +// Copyright 2020-2024 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +// Code generated by lister-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + v1alpha1 "go.pinniped.dev/generated/1.25/apis/supervisor/idp/v1alpha1" + "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/client-go/tools/cache" +) + +// GitHubIdentityProviderLister helps list GitHubIdentityProviders. +// All objects returned here must be treated as read-only. +type GitHubIdentityProviderLister interface { + // List lists all GitHubIdentityProviders in the indexer. + // Objects returned here must be treated as read-only. + List(selector labels.Selector) (ret []*v1alpha1.GitHubIdentityProvider, err error) + // GitHubIdentityProviders returns an object that can list and get GitHubIdentityProviders. + GitHubIdentityProviders(namespace string) GitHubIdentityProviderNamespaceLister + GitHubIdentityProviderListerExpansion +} + +// gitHubIdentityProviderLister implements the GitHubIdentityProviderLister interface. +type gitHubIdentityProviderLister struct { + indexer cache.Indexer +} + +// NewGitHubIdentityProviderLister returns a new GitHubIdentityProviderLister. +func NewGitHubIdentityProviderLister(indexer cache.Indexer) GitHubIdentityProviderLister { + return &gitHubIdentityProviderLister{indexer: indexer} +} + +// List lists all GitHubIdentityProviders in the indexer. +func (s *gitHubIdentityProviderLister) List(selector labels.Selector) (ret []*v1alpha1.GitHubIdentityProvider, err error) { + err = cache.ListAll(s.indexer, selector, func(m interface{}) { + ret = append(ret, m.(*v1alpha1.GitHubIdentityProvider)) + }) + return ret, err +} + +// GitHubIdentityProviders returns an object that can list and get GitHubIdentityProviders. +func (s *gitHubIdentityProviderLister) GitHubIdentityProviders(namespace string) GitHubIdentityProviderNamespaceLister { + return gitHubIdentityProviderNamespaceLister{indexer: s.indexer, namespace: namespace} +} + +// GitHubIdentityProviderNamespaceLister helps list and get GitHubIdentityProviders. +// All objects returned here must be treated as read-only. +type GitHubIdentityProviderNamespaceLister interface { + // List lists all GitHubIdentityProviders in the indexer for a given namespace. + // Objects returned here must be treated as read-only. + List(selector labels.Selector) (ret []*v1alpha1.GitHubIdentityProvider, err error) + // Get retrieves the GitHubIdentityProvider from the indexer for a given namespace and name. + // Objects returned here must be treated as read-only. + Get(name string) (*v1alpha1.GitHubIdentityProvider, error) + GitHubIdentityProviderNamespaceListerExpansion +} + +// gitHubIdentityProviderNamespaceLister implements the GitHubIdentityProviderNamespaceLister +// interface. +type gitHubIdentityProviderNamespaceLister struct { + indexer cache.Indexer + namespace string +} + +// List lists all GitHubIdentityProviders in the indexer for a given namespace. +func (s gitHubIdentityProviderNamespaceLister) List(selector labels.Selector) (ret []*v1alpha1.GitHubIdentityProvider, err error) { + err = cache.ListAllByNamespace(s.indexer, s.namespace, selector, func(m interface{}) { + ret = append(ret, m.(*v1alpha1.GitHubIdentityProvider)) + }) + return ret, err +} + +// Get retrieves the GitHubIdentityProvider from the indexer for a given namespace and name. +func (s gitHubIdentityProviderNamespaceLister) Get(name string) (*v1alpha1.GitHubIdentityProvider, error) { + obj, exists, err := s.indexer.GetByKey(s.namespace + "/" + name) + if err != nil { + return nil, err + } + if !exists { + return nil, errors.NewNotFound(v1alpha1.Resource("githubidentityprovider"), name) + } + return obj.(*v1alpha1.GitHubIdentityProvider), nil +} diff --git a/generated/1.25/crds/authentication.concierge.pinniped.dev_jwtauthenticators.yaml b/generated/1.25/crds/authentication.concierge.pinniped.dev_jwtauthenticators.yaml index b464cfa2b..d59fcb783 100644 --- a/generated/1.25/crds/authentication.concierge.pinniped.dev_jwtauthenticators.yaml +++ b/generated/1.25/crds/authentication.concierge.pinniped.dev_jwtauthenticators.yaml @@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - controller-gen.kubebuilder.io/version: v0.14.0 + controller-gen.kubebuilder.io/version: v0.15.0 name: jwtauthenticators.authentication.concierge.pinniped.dev spec: group: authentication.concierge.pinniped.dev diff --git a/generated/1.25/crds/authentication.concierge.pinniped.dev_webhookauthenticators.yaml b/generated/1.25/crds/authentication.concierge.pinniped.dev_webhookauthenticators.yaml index e8cbc6790..4ccd53770 100644 --- a/generated/1.25/crds/authentication.concierge.pinniped.dev_webhookauthenticators.yaml +++ b/generated/1.25/crds/authentication.concierge.pinniped.dev_webhookauthenticators.yaml @@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - controller-gen.kubebuilder.io/version: v0.14.0 + controller-gen.kubebuilder.io/version: v0.15.0 name: webhookauthenticators.authentication.concierge.pinniped.dev spec: group: authentication.concierge.pinniped.dev diff --git a/generated/1.25/crds/config.concierge.pinniped.dev_credentialissuers.yaml b/generated/1.25/crds/config.concierge.pinniped.dev_credentialissuers.yaml index 8d77a6665..26fed7376 100644 --- a/generated/1.25/crds/config.concierge.pinniped.dev_credentialissuers.yaml +++ b/generated/1.25/crds/config.concierge.pinniped.dev_credentialissuers.yaml @@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - controller-gen.kubebuilder.io/version: v0.14.0 + controller-gen.kubebuilder.io/version: v0.15.0 name: credentialissuers.config.concierge.pinniped.dev spec: group: config.concierge.pinniped.dev diff --git a/generated/1.25/crds/config.supervisor.pinniped.dev_federationdomains.yaml b/generated/1.25/crds/config.supervisor.pinniped.dev_federationdomains.yaml index 7bf231443..d43c7406d 100644 --- a/generated/1.25/crds/config.supervisor.pinniped.dev_federationdomains.yaml +++ b/generated/1.25/crds/config.supervisor.pinniped.dev_federationdomains.yaml @@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - controller-gen.kubebuilder.io/version: v0.14.0 + controller-gen.kubebuilder.io/version: v0.15.0 name: federationdomains.config.supervisor.pinniped.dev spec: group: config.supervisor.pinniped.dev diff --git a/generated/1.25/crds/config.supervisor.pinniped.dev_oidcclients.yaml b/generated/1.25/crds/config.supervisor.pinniped.dev_oidcclients.yaml index d33b31bf1..c44ecbf1e 100644 --- a/generated/1.25/crds/config.supervisor.pinniped.dev_oidcclients.yaml +++ b/generated/1.25/crds/config.supervisor.pinniped.dev_oidcclients.yaml @@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - controller-gen.kubebuilder.io/version: v0.14.0 + controller-gen.kubebuilder.io/version: v0.15.0 name: oidcclients.config.supervisor.pinniped.dev spec: group: config.supervisor.pinniped.dev diff --git a/generated/1.25/crds/idp.supervisor.pinniped.dev_activedirectoryidentityproviders.yaml b/generated/1.25/crds/idp.supervisor.pinniped.dev_activedirectoryidentityproviders.yaml index 414ac4f51..062251102 100644 --- a/generated/1.25/crds/idp.supervisor.pinniped.dev_activedirectoryidentityproviders.yaml +++ b/generated/1.25/crds/idp.supervisor.pinniped.dev_activedirectoryidentityproviders.yaml @@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - controller-gen.kubebuilder.io/version: v0.14.0 + controller-gen.kubebuilder.io/version: v0.15.0 name: activedirectoryidentityproviders.idp.supervisor.pinniped.dev spec: group: idp.supervisor.pinniped.dev diff --git a/generated/1.25/crds/idp.supervisor.pinniped.dev_githubidentityproviders.yaml b/generated/1.25/crds/idp.supervisor.pinniped.dev_githubidentityproviders.yaml new file mode 100644 index 000000000..f93108700 --- /dev/null +++ b/generated/1.25/crds/idp.supervisor.pinniped.dev_githubidentityproviders.yaml @@ -0,0 +1,326 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.15.0 + name: githubidentityproviders.idp.supervisor.pinniped.dev +spec: + group: idp.supervisor.pinniped.dev + names: + categories: + - pinniped + - pinniped-idp + - pinniped-idps + kind: GitHubIdentityProvider + listKind: GitHubIdentityProviderList + plural: githubidentityproviders + singular: githubidentityprovider + scope: Namespaced + versions: + - additionalPrinterColumns: + - jsonPath: .spec.githubAPI.host + name: Host + type: string + - jsonPath: .status.phase + name: Status + type: string + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1alpha1 + schema: + openAPIV3Schema: + description: |- + GitHubIdentityProvider describes the configuration of an upstream GitHub identity provider. + This upstream provider can be configured with either a GitHub App or a GitHub OAuth2 App. + + + Right now, only web-based logins are supported, for both the pinniped-cli client and clients configured + as OIDCClients. + 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: + description: Spec for configuring the identity provider. + properties: + allowAuthentication: + description: AllowAuthentication allows customization of who can authenticate + using this IDP and how. + properties: + organizations: + description: Organizations allows customization of which organizations + can authenticate using this IDP. + properties: + allowed: + description: |- + Allowed, when specified, indicates that only users with membership in at least one of the listed + GitHub organizations may log in. In addition, the group membership presented to Kubernetes will only include + teams within the listed GitHub organizations. Additional login rules or group filtering can optionally be + provided as policy expression on any Pinniped Supervisor FederationDomain that includes this IDP. + + + The configured GitHub App or GitHub OAuth App must be allowed to see membership in the listed organizations, + otherwise Pinniped will not be aware that the user belongs to the listed organization or any teams + within that organization. + + + If no organizations are listed, you must set organizations: AllGitHubUsers. + items: + type: string + maxItems: 64 + type: array + x-kubernetes-list-type: set + policy: + default: OnlyUsersFromAllowedOrganizations + description: |- + Policy must be set to "AllGitHubUsers" if allowed is empty. + + + This field only exists to ensure that Pinniped administrators are aware that an empty list of + allowedOrganizations means all GitHub users are allowed to log in. + enum: + - OnlyUsersFromAllowedOrganizations + - AllGitHubUsers + type: string + type: object + x-kubernetes-validations: + - message: spec.allowAuthentication.organizations.policy must + be 'OnlyUsersFromAllowedOrganizations' when spec.allowAuthentication.organizations.allowed + has organizations listed + rule: '!(has(self.allowed) && size(self.allowed) > 0 && self.policy + == ''AllGitHubUsers'')' + - message: spec.allowAuthentication.organizations.policy must + be 'AllGitHubUsers' when spec.allowAuthentication.organizations.allowed + is empty + rule: '!((!has(self.allowed) || size(self.allowed) == 0) && + self.policy == ''OnlyUsersFromAllowedOrganizations'')' + required: + - organizations + type: object + claims: + default: {} + description: Claims allows customization of the username and groups + claims. + properties: + groups: + default: slug + description: |- + Groups configures which property of the GitHub team record shall determine the group names in Kubernetes. + + + Can be either "name" or "slug". Defaults to "slug". + + + GitHub team names can contain upper and lower case characters, whitespace, and punctuation (e.g. "Kube admins!"). + + + GitHub team slugs are lower case alphanumeric characters and may contain dashes and underscores (e.g. "kube-admins"). + + + Group names as presented to Kubernetes will always be prefixed by the GitHub organization name followed by a + forward slash (e.g. "my-org/my-team"). GitHub organization login names can only contain alphanumeric characters + or single hyphens, so the first forward slash `/` will be the separator between the organization login name and + the team name or slug. + + + If desired, an admin could configure identity transformation expressions on the Pinniped Supervisor's + FederationDomain to further customize how these group names are presented to Kubernetes. + + + See the response schema for + [List teams for the authenticated user](https://docs.github.com/en/rest/teams/teams?apiVersion=2022-11-28#list-teams-for-the-authenticated-user). + enum: + - name + - slug + type: string + username: + default: login:id + description: |- + Username configures which property of the GitHub user record shall determine the username in Kubernetes. + + + Can be either "id", "login", or "login:id". Defaults to "login:id". + + + GitHub's user login attributes can only contain alphanumeric characters and non-repeating hyphens, + and may not start or end with hyphens. GitHub users are allowed to change their login name, + although it is inconvenient. If a GitHub user changed their login name from "foo" to "bar", + then a second user might change their name from "baz" to "foo" in order to take the old + username of the first user. For this reason, it is not as safe to make authorization decisions + based only on the user's login attribute. + + + If desired, an admin could configure identity transformation expressions on the Pinniped Supervisor's + FederationDomain to further customize how these usernames are presented to Kubernetes. + + + Defaults to "login:id", which is the user login attribute, followed by a colon, followed by the unique and + unchanging integer ID number attribute. This blends human-readable login names with the unchanging ID value + from GitHub. Colons are not allowed in GitHub login attributes or ID numbers, so this is a reasonable + choice to concatenate the two values. + + + See the response schema for + [Get the authenticated user](https://docs.github.com/en/rest/users/users?apiVersion=2022-11-28#get-the-authenticated-user). + enum: + - id + - login + - login:id + type: string + type: object + client: + description: Client identifies the secret with credentials for a GitHub + App or GitHub OAuth2 App (a GitHub client). + properties: + secretName: + description: |- + SecretName contains the name of a namespace-local Secret object that provides the clientID and + clientSecret for an GitHub App or GitHub OAuth2 client. + + + This secret must be of type "secrets.pinniped.dev/github-client" with keys "clientID" and "clientSecret". + minLength: 1 + type: string + required: + - secretName + type: object + githubAPI: + default: {} + description: GitHubAPI allows configuration for GitHub Enterprise + Server + properties: + host: + default: github.com + description: |- + Host is required only for GitHub Enterprise Server. + Defaults to using GitHub's public API ("github.com"). + Do not specify a protocol or scheme since "https://" will always be used. + Port is optional. Do not specify a path, query, fragment, or userinfo. + Only domain name or IP address, subdomains (optional), and port (optional). + IPv4 and IPv6 are supported. If using an IPv6 address with a port, you must enclose the IPv6 address + in square brackets. Example: "[::1]:443". + minLength: 1 + type: string + tls: + description: TLS configuration for GitHub Enterprise Server. + properties: + certificateAuthorityData: + description: X.509 Certificate Authority (base64-encoded PEM + bundle). If omitted, a default set of system roots will + be trusted. + type: string + type: object + type: object + required: + - allowAuthentication + - client + type: object + status: + description: Status of the identity provider. + properties: + conditions: + description: Conditions represents the observations of an identity + provider's current state. + items: + description: "Condition contains details for one aspect of the current + state of this API Resource.\n---\nThis struct is intended for + direct use as an array at the field path .status.conditions. For + example,\n\n\n\ttype FooStatus struct{\n\t // Represents the + observations of a foo's current state.\n\t // Known .status.conditions.type + are: \"Available\", \"Progressing\", and \"Degraded\"\n\t // + +patchMergeKey=type\n\t // +patchStrategy=merge\n\t // +listType=map\n\t + \ // +listMapKey=type\n\t Conditions []metav1.Condition `json:\"conditions,omitempty\" + patchStrategy:\"merge\" patchMergeKey:\"type\" protobuf:\"bytes,1,rep,name=conditions\"`\n\n\n\t + \ // other fields\n\t}" + properties: + lastTransitionTime: + description: |- + lastTransitionTime is the last time the condition transitioned from one status to another. + This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: |- + message is a human readable message indicating details about the transition. + This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: |- + observedGeneration represents the .metadata.generation that the condition was set based upon. + For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date + with respect to the current state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: |- + reason contains a programmatic identifier indicating the reason for the condition's last transition. + Producers of specific condition types may define expected values and meanings for this field, + and whether the values are considered a guaranteed API. + The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: |- + type of condition in CamelCase or in foo.example.com/CamelCase. + --- + Many .condition.type values are consistent across resources like Available, but because arbitrary conditions can be + useful (see .node.status.conditions), the ability to deconflict is important. + The regex it matches is (dns1123SubdomainFmt/)?(qualifiedNameFmt) + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + x-kubernetes-list-map-keys: + - type + x-kubernetes-list-type: map + phase: + default: Pending + description: Phase summarizes the overall status of the GitHubIdentityProvider. + enum: + - Pending + - Ready + - Error + type: string + type: object + required: + - spec + type: object + served: true + storage: true + subresources: + status: {} diff --git a/generated/1.25/crds/idp.supervisor.pinniped.dev_ldapidentityproviders.yaml b/generated/1.25/crds/idp.supervisor.pinniped.dev_ldapidentityproviders.yaml index f718e93aa..711e9a754 100644 --- a/generated/1.25/crds/idp.supervisor.pinniped.dev_ldapidentityproviders.yaml +++ b/generated/1.25/crds/idp.supervisor.pinniped.dev_ldapidentityproviders.yaml @@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - controller-gen.kubebuilder.io/version: v0.14.0 + controller-gen.kubebuilder.io/version: v0.15.0 name: ldapidentityproviders.idp.supervisor.pinniped.dev spec: group: idp.supervisor.pinniped.dev diff --git a/generated/1.25/crds/idp.supervisor.pinniped.dev_oidcidentityproviders.yaml b/generated/1.25/crds/idp.supervisor.pinniped.dev_oidcidentityproviders.yaml index 486665605..acfca1573 100644 --- a/generated/1.25/crds/idp.supervisor.pinniped.dev_oidcidentityproviders.yaml +++ b/generated/1.25/crds/idp.supervisor.pinniped.dev_oidcidentityproviders.yaml @@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - controller-gen.kubebuilder.io/version: v0.14.0 + controller-gen.kubebuilder.io/version: v0.15.0 name: oidcidentityproviders.idp.supervisor.pinniped.dev spec: group: idp.supervisor.pinniped.dev diff --git a/generated/1.26/README.adoc b/generated/1.26/README.adoc index f1de27af4..a3f58fc82 100644 --- a/generated/1.26/README.adoc +++ b/generated/1.26/README.adoc @@ -1645,6 +1645,285 @@ Optional, when empty this defaults to "objectGUID". + |=== +[id="{anchor_prefix}-go-pinniped-dev-generated-1-26-apis-supervisor-idp-v1alpha1-githubapiconfig"] +==== GitHubAPIConfig + +GitHubAPIConfig allows configuration for GitHub Enterprise Server + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-26-apis-supervisor-idp-v1alpha1-githubidentityproviderspec[$$GitHubIdentityProviderSpec$$] +**** + +[cols="25a,75a", options="header"] +|=== +| Field | Description +| *`host`* __string__ | Host is required only for GitHub Enterprise Server. + +Defaults to using GitHub's public API ("github.com"). + +Do not specify a protocol or scheme since "https://" will always be used. + +Port is optional. Do not specify a path, query, fragment, or userinfo. + +Only domain name or IP address, subdomains (optional), and port (optional). + +IPv4 and IPv6 are supported. If using an IPv6 address with a port, you must enclose the IPv6 address + +in square brackets. Example: "[::1]:443". + +| *`tls`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-26-apis-supervisor-idp-v1alpha1-tlsspec[$$TLSSpec$$]__ | TLS configuration for GitHub Enterprise Server. + +|=== + + +[id="{anchor_prefix}-go-pinniped-dev-generated-1-26-apis-supervisor-idp-v1alpha1-githuballowauthenticationspec"] +==== GitHubAllowAuthenticationSpec + +GitHubAllowAuthenticationSpec allows customization of who can authenticate using this IDP and how. + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-26-apis-supervisor-idp-v1alpha1-githubidentityproviderspec[$$GitHubIdentityProviderSpec$$] +**** + +[cols="25a,75a", options="header"] +|=== +| Field | Description +| *`organizations`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-26-apis-supervisor-idp-v1alpha1-githuborganizationsspec[$$GitHubOrganizationsSpec$$]__ | Organizations allows customization of which organizations can authenticate using this IDP. + +|=== + + +[id="{anchor_prefix}-go-pinniped-dev-generated-1-26-apis-supervisor-idp-v1alpha1-githuballowedauthorganizationspolicy"] +==== GitHubAllowedAuthOrganizationsPolicy (string) + + + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-26-apis-supervisor-idp-v1alpha1-githuborganizationsspec[$$GitHubOrganizationsSpec$$] +**** + + + +[id="{anchor_prefix}-go-pinniped-dev-generated-1-26-apis-supervisor-idp-v1alpha1-githubclaims"] +==== GitHubClaims + +GitHubClaims allows customization of the username and groups claims. + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-26-apis-supervisor-idp-v1alpha1-githubidentityproviderspec[$$GitHubIdentityProviderSpec$$] +**** + +[cols="25a,75a", options="header"] +|=== +| Field | Description +| *`username`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-26-apis-supervisor-idp-v1alpha1-githubusernameattribute[$$GitHubUsernameAttribute$$]__ | Username configures which property of the GitHub user record shall determine the username in Kubernetes. + + + +Can be either "id", "login", or "login:id". Defaults to "login:id". + + + +GitHub's user login attributes can only contain alphanumeric characters and non-repeating hyphens, + +and may not start or end with hyphens. GitHub users are allowed to change their login name, + +although it is inconvenient. If a GitHub user changed their login name from "foo" to "bar", + +then a second user might change their name from "baz" to "foo" in order to take the old + +username of the first user. For this reason, it is not as safe to make authorization decisions + +based only on the user's login attribute. + + + +If desired, an admin could configure identity transformation expressions on the Pinniped Supervisor's + +FederationDomain to further customize how these usernames are presented to Kubernetes. + + + +Defaults to "login:id", which is the user login attribute, followed by a colon, followed by the unique and + +unchanging integer ID number attribute. This blends human-readable login names with the unchanging ID value + +from GitHub. Colons are not allowed in GitHub login attributes or ID numbers, so this is a reasonable + +choice to concatenate the two values. + + + +See the response schema for + +[Get the authenticated user](https://docs.github.com/en/rest/users/users?apiVersion=2022-11-28#get-the-authenticated-user). + +| *`groups`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-26-apis-supervisor-idp-v1alpha1-githubgroupnameattribute[$$GitHubGroupNameAttribute$$]__ | Groups configures which property of the GitHub team record shall determine the group names in Kubernetes. + + + +Can be either "name" or "slug". Defaults to "slug". + + + +GitHub team names can contain upper and lower case characters, whitespace, and punctuation (e.g. "Kube admins!"). + + + +GitHub team slugs are lower case alphanumeric characters and may contain dashes and underscores (e.g. "kube-admins"). + + + +Group names as presented to Kubernetes will always be prefixed by the GitHub organization name followed by a + +forward slash (e.g. "my-org/my-team"). GitHub organization login names can only contain alphanumeric characters + +or single hyphens, so the first forward slash `/` will be the separator between the organization login name and + +the team name or slug. + + + +If desired, an admin could configure identity transformation expressions on the Pinniped Supervisor's + +FederationDomain to further customize how these group names are presented to Kubernetes. + + + +See the response schema for + +[List teams for the authenticated user](https://docs.github.com/en/rest/teams/teams?apiVersion=2022-11-28#list-teams-for-the-authenticated-user). + +|=== + + +[id="{anchor_prefix}-go-pinniped-dev-generated-1-26-apis-supervisor-idp-v1alpha1-githubclientspec"] +==== GitHubClientSpec + +GitHubClientSpec contains information about the GitHub client that this identity provider will use +for web-based login flows. + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-26-apis-supervisor-idp-v1alpha1-githubidentityproviderspec[$$GitHubIdentityProviderSpec$$] +**** + +[cols="25a,75a", options="header"] +|=== +| Field | Description +| *`secretName`* __string__ | SecretName contains the name of a namespace-local Secret object that provides the clientID and + +clientSecret for an GitHub App or GitHub OAuth2 client. + + + +This secret must be of type "secrets.pinniped.dev/github-client" with keys "clientID" and "clientSecret". + +|=== + + +[id="{anchor_prefix}-go-pinniped-dev-generated-1-26-apis-supervisor-idp-v1alpha1-githubgroupnameattribute"] +==== GitHubGroupNameAttribute (string) + +GitHubGroupNameAttribute allows the user to specify which attribute from GitHub to use for the group +names to present to Kubernetes. See the response schema for +[List teams for the authenticated user](https://docs.github.com/en/rest/teams/teams?apiVersion=2022-11-28#list-teams-for-the-authenticated-user). + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-26-apis-supervisor-idp-v1alpha1-githubclaims[$$GitHubClaims$$] +**** + + + +[id="{anchor_prefix}-go-pinniped-dev-generated-1-26-apis-supervisor-idp-v1alpha1-githubidentityprovider"] +==== GitHubIdentityProvider + +GitHubIdentityProvider describes the configuration of an upstream GitHub identity provider. +This upstream provider can be configured with either a GitHub App or a GitHub OAuth2 App. + + +Right now, only web-based logins are supported, for both the pinniped-cli client and clients configured +as OIDCClients. + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-26-apis-supervisor-idp-v1alpha1-githubidentityproviderlist[$$GitHubIdentityProviderList$$] +**** + +[cols="25a,75a", options="header"] +|=== +| Field | Description +| *`metadata`* __link:https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.26/#objectmeta-v1-meta[$$ObjectMeta$$]__ | Refer to Kubernetes API documentation for fields of `metadata`. + +| *`spec`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-26-apis-supervisor-idp-v1alpha1-githubidentityproviderspec[$$GitHubIdentityProviderSpec$$]__ | Spec for configuring the identity provider. + +| *`status`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-26-apis-supervisor-idp-v1alpha1-githubidentityproviderstatus[$$GitHubIdentityProviderStatus$$]__ | Status of the identity provider. + +|=== + + + + +[id="{anchor_prefix}-go-pinniped-dev-generated-1-26-apis-supervisor-idp-v1alpha1-githubidentityproviderphase"] +==== GitHubIdentityProviderPhase (string) + + + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-26-apis-supervisor-idp-v1alpha1-githubidentityproviderstatus[$$GitHubIdentityProviderStatus$$] +**** + + + +[id="{anchor_prefix}-go-pinniped-dev-generated-1-26-apis-supervisor-idp-v1alpha1-githubidentityproviderspec"] +==== GitHubIdentityProviderSpec + +GitHubIdentityProviderSpec is the spec for configuring an GitHub identity provider. + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-26-apis-supervisor-idp-v1alpha1-githubidentityprovider[$$GitHubIdentityProvider$$] +**** + +[cols="25a,75a", options="header"] +|=== +| Field | Description +| *`githubAPI`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-26-apis-supervisor-idp-v1alpha1-githubapiconfig[$$GitHubAPIConfig$$]__ | GitHubAPI allows configuration for GitHub Enterprise Server + +| *`claims`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-26-apis-supervisor-idp-v1alpha1-githubclaims[$$GitHubClaims$$]__ | Claims allows customization of the username and groups claims. + +| *`allowAuthentication`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-26-apis-supervisor-idp-v1alpha1-githuballowauthenticationspec[$$GitHubAllowAuthenticationSpec$$]__ | AllowAuthentication allows customization of who can authenticate using this IDP and how. + +| *`client`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-26-apis-supervisor-idp-v1alpha1-githubclientspec[$$GitHubClientSpec$$]__ | Client identifies the secret with credentials for a GitHub App or GitHub OAuth2 App (a GitHub client). + +|=== + + +[id="{anchor_prefix}-go-pinniped-dev-generated-1-26-apis-supervisor-idp-v1alpha1-githubidentityproviderstatus"] +==== GitHubIdentityProviderStatus + +GitHubIdentityProviderStatus is the status of an GitHub identity provider. + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-26-apis-supervisor-idp-v1alpha1-githubidentityprovider[$$GitHubIdentityProvider$$] +**** + +[cols="25a,75a", options="header"] +|=== +| Field | Description +| *`phase`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-26-apis-supervisor-idp-v1alpha1-githubidentityproviderphase[$$GitHubIdentityProviderPhase$$]__ | Phase summarizes the overall status of the GitHubIdentityProvider. + +| *`conditions`* __link:https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.26/#condition-v1-meta[$$Condition$$] array__ | Conditions represents the observations of an identity provider's current state. + +|=== + + +[id="{anchor_prefix}-go-pinniped-dev-generated-1-26-apis-supervisor-idp-v1alpha1-githuborganizationsspec"] +==== GitHubOrganizationsSpec + + + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-26-apis-supervisor-idp-v1alpha1-githuballowauthenticationspec[$$GitHubAllowAuthenticationSpec$$] +**** + +[cols="25a,75a", options="header"] +|=== +| Field | Description +| *`policy`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-26-apis-supervisor-idp-v1alpha1-githuballowedauthorganizationspolicy[$$GitHubAllowedAuthOrganizationsPolicy$$]__ | Policy must be set to "AllGitHubUsers" if allowed is empty. + + + +This field only exists to ensure that Pinniped administrators are aware that an empty list of + +allowedOrganizations means all GitHub users are allowed to log in. + +| *`allowed`* __string array__ | Allowed, when specified, indicates that only users with membership in at least one of the listed + +GitHub organizations may log in. In addition, the group membership presented to Kubernetes will only include + +teams within the listed GitHub organizations. Additional login rules or group filtering can optionally be + +provided as policy expression on any Pinniped Supervisor FederationDomain that includes this IDP. + + + +The configured GitHub App or GitHub OAuth App must be allowed to see membership in the listed organizations, + +otherwise Pinniped will not be aware that the user belongs to the listed organization or any teams + +within that organization. + + + +If no organizations are listed, you must set organizations: AllGitHubUsers. + +|=== + + +[id="{anchor_prefix}-go-pinniped-dev-generated-1-26-apis-supervisor-idp-v1alpha1-githubusernameattribute"] +==== GitHubUsernameAttribute (string) + +GitHubUsernameAttribute allows the user to specify which attribute(s) from GitHub to use for the username to present +to Kubernetes. See the response schema for +[Get the authenticated user](https://docs.github.com/en/rest/users/users?apiVersion=2022-11-28#get-the-authenticated-user). + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-26-apis-supervisor-idp-v1alpha1-githubclaims[$$GitHubClaims$$] +**** + + + [id="{anchor_prefix}-go-pinniped-dev-generated-1-26-apis-supervisor-idp-v1alpha1-ldapidentityprovider"] ==== LDAPIdentityProvider @@ -2108,11 +2387,12 @@ Parameter is a key/value pair which represents a parameter in an HTTP request. [id="{anchor_prefix}-go-pinniped-dev-generated-1-26-apis-supervisor-idp-v1alpha1-tlsspec"] ==== TLSSpec -Configuration for TLS parameters related to identity provider integration. +TLSSpec provides TLS configuration for identity provider integration. .Appears In: **** - xref:{anchor_prefix}-go-pinniped-dev-generated-1-26-apis-supervisor-idp-v1alpha1-activedirectoryidentityproviderspec[$$ActiveDirectoryIdentityProviderSpec$$] +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-26-apis-supervisor-idp-v1alpha1-githubapiconfig[$$GitHubAPIConfig$$] - xref:{anchor_prefix}-go-pinniped-dev-generated-1-26-apis-supervisor-idp-v1alpha1-ldapidentityproviderspec[$$LDAPIdentityProviderSpec$$] - xref:{anchor_prefix}-go-pinniped-dev-generated-1-26-apis-supervisor-idp-v1alpha1-oidcidentityproviderspec[$$OIDCIdentityProviderSpec$$] **** diff --git a/generated/1.26/apis/supervisor/idp/v1alpha1/register.go b/generated/1.26/apis/supervisor/idp/v1alpha1/register.go index 8829a8638..705be8076 100644 --- a/generated/1.26/apis/supervisor/idp/v1alpha1/register.go +++ b/generated/1.26/apis/supervisor/idp/v1alpha1/register.go @@ -1,4 +1,4 @@ -// Copyright 2020-2022 the Pinniped contributors. All Rights Reserved. +// Copyright 2020-2024 the Pinniped contributors. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 package v1alpha1 @@ -36,6 +36,8 @@ func addKnownTypes(scheme *runtime.Scheme) error { &LDAPIdentityProviderList{}, &ActiveDirectoryIdentityProvider{}, &ActiveDirectoryIdentityProviderList{}, + &GitHubIdentityProvider{}, + &GitHubIdentityProviderList{}, ) metav1.AddToGroupVersion(scheme, SchemeGroupVersion) return nil diff --git a/generated/1.26/apis/supervisor/idp/v1alpha1/types_githubidentityprovider.go b/generated/1.26/apis/supervisor/idp/v1alpha1/types_githubidentityprovider.go new file mode 100644 index 000000000..c84f46dbd --- /dev/null +++ b/generated/1.26/apis/supervisor/idp/v1alpha1/types_githubidentityprovider.go @@ -0,0 +1,256 @@ +// Copyright 2024 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package v1alpha1 + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +type GitHubIdentityProviderPhase string + +const ( + // GitHubPhasePending is the default phase for newly-created GitHubIdentityProvider resources. + GitHubPhasePending GitHubIdentityProviderPhase = "Pending" + + // GitHubPhaseReady is the phase for an GitHubIdentityProvider resource in a healthy state. + GitHubPhaseReady GitHubIdentityProviderPhase = "Ready" + + // GitHubPhaseError is the phase for an GitHubIdentityProvider in an unhealthy state. + GitHubPhaseError GitHubIdentityProviderPhase = "Error" +) + +type GitHubAllowedAuthOrganizationsPolicy string + +const ( + // GitHubAllowedAuthOrganizationsPolicyAllGitHubUsers means any GitHub user is allowed to log in using this identity + // provider, regardless of their organization membership or lack thereof. + GitHubAllowedAuthOrganizationsPolicyAllGitHubUsers GitHubAllowedAuthOrganizationsPolicy = "AllGitHubUsers" + + // GitHubAllowedAuthOrganizationsPolicyOnlyUsersFromAllowedOrganizations means only those users with membership in + // the listed GitHub organizations are allowed to log in. + GitHubAllowedAuthOrganizationsPolicyOnlyUsersFromAllowedOrganizations GitHubAllowedAuthOrganizationsPolicy = "OnlyUsersFromAllowedOrganizations" +) + +// GitHubIdentityProviderStatus is the status of an GitHub identity provider. +type GitHubIdentityProviderStatus struct { + // Phase summarizes the overall status of the GitHubIdentityProvider. + // + // +kubebuilder:default=Pending + // +kubebuilder:validation:Enum=Pending;Ready;Error + Phase GitHubIdentityProviderPhase `json:"phase,omitempty"` + + // Conditions represents the observations of an identity provider's current state. + // + // +patchMergeKey=type + // +patchStrategy=merge + // +listType=map + // +listMapKey=type + Conditions []metav1.Condition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type"` +} + +// GitHubAPIConfig allows configuration for GitHub Enterprise Server +type GitHubAPIConfig struct { + // Host is required only for GitHub Enterprise Server. + // Defaults to using GitHub's public API ("github.com"). + // Do not specify a protocol or scheme since "https://" will always be used. + // Port is optional. Do not specify a path, query, fragment, or userinfo. + // Only domain name or IP address, subdomains (optional), and port (optional). + // IPv4 and IPv6 are supported. If using an IPv6 address with a port, you must enclose the IPv6 address + // in square brackets. Example: "[::1]:443". + // + // +kubebuilder:default="github.com" + // +kubebuilder:validation:MinLength=1 + // +optional + Host *string `json:"host"` + + // TLS configuration for GitHub Enterprise Server. + // + // +optional + TLS *TLSSpec `json:"tls,omitempty"` +} + +// GitHubUsernameAttribute allows the user to specify which attribute(s) from GitHub to use for the username to present +// to Kubernetes. See the response schema for +// [Get the authenticated user](https://docs.github.com/en/rest/users/users?apiVersion=2022-11-28#get-the-authenticated-user). +type GitHubUsernameAttribute string + +const ( + // GitHubUsernameID specifies using the `id` attribute from the GitHub user for the username to present to Kubernetes. + GitHubUsernameID GitHubUsernameAttribute = "id" + + // GitHubUsernameLogin specifies using the `login` attribute from the GitHub user as the username to present to Kubernetes. + GitHubUsernameLogin GitHubUsernameAttribute = "login" + + // GitHubUsernameLoginAndID specifies combining the `login` and `id` attributes from the GitHub user as the + // username to present to Kubernetes, separated by a colon. Example: "my-login:1234" + GitHubUsernameLoginAndID GitHubUsernameAttribute = "login:id" +) + +// GitHubGroupNameAttribute allows the user to specify which attribute from GitHub to use for the group +// names to present to Kubernetes. See the response schema for +// [List teams for the authenticated user](https://docs.github.com/en/rest/teams/teams?apiVersion=2022-11-28#list-teams-for-the-authenticated-user). +type GitHubGroupNameAttribute string + +const ( + // GitHubUseTeamNameForGroupName specifies using the GitHub team's `name` attribute as the group name to present to Kubernetes. + GitHubUseTeamNameForGroupName GitHubGroupNameAttribute = "name" + + // GitHubUseTeamSlugForGroupName specifies using the GitHub team's `slug` attribute as the group name to present to Kubernetes. + GitHubUseTeamSlugForGroupName GitHubGroupNameAttribute = "slug" +) + +// GitHubClaims allows customization of the username and groups claims. +type GitHubClaims struct { + // Username configures which property of the GitHub user record shall determine the username in Kubernetes. + // + // Can be either "id", "login", or "login:id". Defaults to "login:id". + // + // GitHub's user login attributes can only contain alphanumeric characters and non-repeating hyphens, + // and may not start or end with hyphens. GitHub users are allowed to change their login name, + // although it is inconvenient. If a GitHub user changed their login name from "foo" to "bar", + // then a second user might change their name from "baz" to "foo" in order to take the old + // username of the first user. For this reason, it is not as safe to make authorization decisions + // based only on the user's login attribute. + // + // If desired, an admin could configure identity transformation expressions on the Pinniped Supervisor's + // FederationDomain to further customize how these usernames are presented to Kubernetes. + // + // Defaults to "login:id", which is the user login attribute, followed by a colon, followed by the unique and + // unchanging integer ID number attribute. This blends human-readable login names with the unchanging ID value + // from GitHub. Colons are not allowed in GitHub login attributes or ID numbers, so this is a reasonable + // choice to concatenate the two values. + // + // See the response schema for + // [Get the authenticated user](https://docs.github.com/en/rest/users/users?apiVersion=2022-11-28#get-the-authenticated-user). + // + // +kubebuilder:default="login:id" + // +kubebuilder:validation:Enum={"id","login","login:id"} + // +optional + Username *GitHubUsernameAttribute `json:"username"` + + // Groups configures which property of the GitHub team record shall determine the group names in Kubernetes. + // + // Can be either "name" or "slug". Defaults to "slug". + // + // GitHub team names can contain upper and lower case characters, whitespace, and punctuation (e.g. "Kube admins!"). + // + // GitHub team slugs are lower case alphanumeric characters and may contain dashes and underscores (e.g. "kube-admins"). + // + // Group names as presented to Kubernetes will always be prefixed by the GitHub organization name followed by a + // forward slash (e.g. "my-org/my-team"). GitHub organization login names can only contain alphanumeric characters + // or single hyphens, so the first forward slash `/` will be the separator between the organization login name and + // the team name or slug. + // + // If desired, an admin could configure identity transformation expressions on the Pinniped Supervisor's + // FederationDomain to further customize how these group names are presented to Kubernetes. + // + // See the response schema for + // [List teams for the authenticated user](https://docs.github.com/en/rest/teams/teams?apiVersion=2022-11-28#list-teams-for-the-authenticated-user). + // + // +kubebuilder:default=slug + // +kubebuilder:validation:Enum=name;slug + // +optional + Groups *GitHubGroupNameAttribute `json:"groups"` +} + +// GitHubClientSpec contains information about the GitHub client that this identity provider will use +// for web-based login flows. +type GitHubClientSpec struct { + // SecretName contains the name of a namespace-local Secret object that provides the clientID and + // clientSecret for an GitHub App or GitHub OAuth2 client. + // + // This secret must be of type "secrets.pinniped.dev/github-client" with keys "clientID" and "clientSecret". + // + // +kubebuilder:validation:MinLength=1 + SecretName string `json:"secretName"` +} + +type GitHubOrganizationsSpec struct { + // Policy must be set to "AllGitHubUsers" if allowed is empty. + // + // This field only exists to ensure that Pinniped administrators are aware that an empty list of + // allowedOrganizations means all GitHub users are allowed to log in. + // + // +kubebuilder:default=OnlyUsersFromAllowedOrganizations + // +kubebuilder:validation:Enum=OnlyUsersFromAllowedOrganizations;AllGitHubUsers + // +optional + Policy *GitHubAllowedAuthOrganizationsPolicy `json:"policy"` + + // Allowed, when specified, indicates that only users with membership in at least one of the listed + // GitHub organizations may log in. In addition, the group membership presented to Kubernetes will only include + // teams within the listed GitHub organizations. Additional login rules or group filtering can optionally be + // provided as policy expression on any Pinniped Supervisor FederationDomain that includes this IDP. + // + // The configured GitHub App or GitHub OAuth App must be allowed to see membership in the listed organizations, + // otherwise Pinniped will not be aware that the user belongs to the listed organization or any teams + // within that organization. + // + // If no organizations are listed, you must set organizations: AllGitHubUsers. + // + // +kubebuilder:validation:MaxItems=64 + // +listType=set + // +optional + Allowed []string `json:"allowed,omitempty"` +} + +// GitHubAllowAuthenticationSpec allows customization of who can authenticate using this IDP and how. +type GitHubAllowAuthenticationSpec struct { + // Organizations allows customization of which organizations can authenticate using this IDP. + // +kubebuilder:validation:XValidation:message="spec.allowAuthentication.organizations.policy must be 'OnlyUsersFromAllowedOrganizations' when spec.allowAuthentication.organizations.allowed has organizations listed",rule="!(has(self.allowed) && size(self.allowed) > 0 && self.policy == 'AllGitHubUsers')" + // +kubebuilder:validation:XValidation:message="spec.allowAuthentication.organizations.policy must be 'AllGitHubUsers' when spec.allowAuthentication.organizations.allowed is empty",rule="!((!has(self.allowed) || size(self.allowed) == 0) && self.policy == 'OnlyUsersFromAllowedOrganizations')" + Organizations GitHubOrganizationsSpec `json:"organizations"` +} + +// GitHubIdentityProviderSpec is the spec for configuring an GitHub identity provider. +type GitHubIdentityProviderSpec struct { + // GitHubAPI allows configuration for GitHub Enterprise Server + // + // +kubebuilder:default={} + GitHubAPI GitHubAPIConfig `json:"githubAPI,omitempty"` + + // Claims allows customization of the username and groups claims. + // + // +kubebuilder:default={} + Claims GitHubClaims `json:"claims,omitempty"` + + // AllowAuthentication allows customization of who can authenticate using this IDP and how. + AllowAuthentication GitHubAllowAuthenticationSpec `json:"allowAuthentication"` + + // Client identifies the secret with credentials for a GitHub App or GitHub OAuth2 App (a GitHub client). + Client GitHubClientSpec `json:"client"` +} + +// GitHubIdentityProvider describes the configuration of an upstream GitHub identity provider. +// This upstream provider can be configured with either a GitHub App or a GitHub OAuth2 App. +// +// Right now, only web-based logins are supported, for both the pinniped-cli client and clients configured +// as OIDCClients. +// +// +genclient +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object +// +kubebuilder:resource:categories=pinniped;pinniped-idp;pinniped-idps +// +kubebuilder:printcolumn:name="Host",type=string,JSONPath=`.spec.githubAPI.host` +// +kubebuilder:printcolumn:name="Status",type=string,JSONPath=`.status.phase` +// +kubebuilder:printcolumn:name="Age",type=date,JSONPath=`.metadata.creationTimestamp` +// +kubebuilder:subresource:status +type GitHubIdentityProvider struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + // Spec for configuring the identity provider. + Spec GitHubIdentityProviderSpec `json:"spec"` + + // Status of the identity provider. + Status GitHubIdentityProviderStatus `json:"status,omitempty"` +} + +// GitHubIdentityProviderList lists GitHubIdentityProvider objects. +// +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object +type GitHubIdentityProviderList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + + Items []GitHubIdentityProvider `json:"items"` +} diff --git a/generated/1.26/apis/supervisor/idp/v1alpha1/types_tls.go b/generated/1.26/apis/supervisor/idp/v1alpha1/types_tls.go index 1413a262c..49b49373c 100644 --- a/generated/1.26/apis/supervisor/idp/v1alpha1/types_tls.go +++ b/generated/1.26/apis/supervisor/idp/v1alpha1/types_tls.go @@ -1,9 +1,9 @@ -// Copyright 2020-2022 the Pinniped contributors. All Rights Reserved. +// Copyright 2020-2024 the Pinniped contributors. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 package v1alpha1 -// Configuration for TLS parameters related to identity provider integration. +// TLSSpec provides TLS configuration for identity provider integration. type TLSSpec struct { // X.509 Certificate Authority (base64-encoded PEM bundle). If omitted, a default set of system roots will be trusted. // +optional diff --git a/generated/1.26/apis/supervisor/idp/v1alpha1/zz_generated.deepcopy.go b/generated/1.26/apis/supervisor/idp/v1alpha1/zz_generated.deepcopy.go index b0cb168b5..e48860e82 100644 --- a/generated/1.26/apis/supervisor/idp/v1alpha1/zz_generated.deepcopy.go +++ b/generated/1.26/apis/supervisor/idp/v1alpha1/zz_generated.deepcopy.go @@ -203,6 +203,221 @@ func (in *ActiveDirectoryIdentityProviderUserSearchAttributes) DeepCopy() *Activ return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *GitHubAPIConfig) DeepCopyInto(out *GitHubAPIConfig) { + *out = *in + if in.Host != nil { + in, out := &in.Host, &out.Host + *out = new(string) + **out = **in + } + if in.TLS != nil { + in, out := &in.TLS, &out.TLS + *out = new(TLSSpec) + **out = **in + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GitHubAPIConfig. +func (in *GitHubAPIConfig) DeepCopy() *GitHubAPIConfig { + if in == nil { + return nil + } + out := new(GitHubAPIConfig) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *GitHubAllowAuthenticationSpec) DeepCopyInto(out *GitHubAllowAuthenticationSpec) { + *out = *in + in.Organizations.DeepCopyInto(&out.Organizations) + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GitHubAllowAuthenticationSpec. +func (in *GitHubAllowAuthenticationSpec) DeepCopy() *GitHubAllowAuthenticationSpec { + if in == nil { + return nil + } + out := new(GitHubAllowAuthenticationSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *GitHubClaims) DeepCopyInto(out *GitHubClaims) { + *out = *in + if in.Username != nil { + in, out := &in.Username, &out.Username + *out = new(GitHubUsernameAttribute) + **out = **in + } + if in.Groups != nil { + in, out := &in.Groups, &out.Groups + *out = new(GitHubGroupNameAttribute) + **out = **in + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GitHubClaims. +func (in *GitHubClaims) DeepCopy() *GitHubClaims { + if in == nil { + return nil + } + out := new(GitHubClaims) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *GitHubClientSpec) DeepCopyInto(out *GitHubClientSpec) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GitHubClientSpec. +func (in *GitHubClientSpec) DeepCopy() *GitHubClientSpec { + if in == nil { + return nil + } + out := new(GitHubClientSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *GitHubIdentityProvider) DeepCopyInto(out *GitHubIdentityProvider) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + in.Status.DeepCopyInto(&out.Status) + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GitHubIdentityProvider. +func (in *GitHubIdentityProvider) DeepCopy() *GitHubIdentityProvider { + if in == nil { + return nil + } + out := new(GitHubIdentityProvider) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *GitHubIdentityProvider) 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 *GitHubIdentityProviderList) DeepCopyInto(out *GitHubIdentityProviderList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]GitHubIdentityProvider, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GitHubIdentityProviderList. +func (in *GitHubIdentityProviderList) DeepCopy() *GitHubIdentityProviderList { + if in == nil { + return nil + } + out := new(GitHubIdentityProviderList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *GitHubIdentityProviderList) 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 *GitHubIdentityProviderSpec) DeepCopyInto(out *GitHubIdentityProviderSpec) { + *out = *in + in.GitHubAPI.DeepCopyInto(&out.GitHubAPI) + in.Claims.DeepCopyInto(&out.Claims) + in.AllowAuthentication.DeepCopyInto(&out.AllowAuthentication) + out.Client = in.Client + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GitHubIdentityProviderSpec. +func (in *GitHubIdentityProviderSpec) DeepCopy() *GitHubIdentityProviderSpec { + if in == nil { + return nil + } + out := new(GitHubIdentityProviderSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *GitHubIdentityProviderStatus) DeepCopyInto(out *GitHubIdentityProviderStatus) { + *out = *in + if in.Conditions != nil { + in, out := &in.Conditions, &out.Conditions + *out = make([]v1.Condition, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GitHubIdentityProviderStatus. +func (in *GitHubIdentityProviderStatus) DeepCopy() *GitHubIdentityProviderStatus { + if in == nil { + return nil + } + out := new(GitHubIdentityProviderStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *GitHubOrganizationsSpec) DeepCopyInto(out *GitHubOrganizationsSpec) { + *out = *in + if in.Policy != nil { + in, out := &in.Policy, &out.Policy + *out = new(GitHubAllowedAuthOrganizationsPolicy) + **out = **in + } + if in.Allowed != nil { + in, out := &in.Allowed, &out.Allowed + *out = make([]string, len(*in)) + copy(*out, *in) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GitHubOrganizationsSpec. +func (in *GitHubOrganizationsSpec) DeepCopy() *GitHubOrganizationsSpec { + if in == nil { + return nil + } + out := new(GitHubOrganizationsSpec) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *LDAPIdentityProvider) DeepCopyInto(out *LDAPIdentityProvider) { *out = *in diff --git a/generated/1.26/apis/supervisor/idpdiscovery/v1alpha1/types_supervisor_idp_discovery.go b/generated/1.26/apis/supervisor/idpdiscovery/v1alpha1/types_supervisor_idp_discovery.go index 837fed2dd..11be6f9dc 100644 --- a/generated/1.26/apis/supervisor/idpdiscovery/v1alpha1/types_supervisor_idp_discovery.go +++ b/generated/1.26/apis/supervisor/idpdiscovery/v1alpha1/types_supervisor_idp_discovery.go @@ -15,6 +15,7 @@ const ( IDPTypeOIDC IDPType = "oidc" IDPTypeLDAP IDPType = "ldap" IDPTypeActiveDirectory IDPType = "activedirectory" + IDPTypeGitHub IDPType = "github" IDPFlowCLIPassword IDPFlow = "cli_password" IDPFlowBrowserAuthcode IDPFlow = "browser_authcode" diff --git a/generated/1.26/client/supervisor/clientset/versioned/typed/idp/v1alpha1/fake/fake_githubidentityprovider.go b/generated/1.26/client/supervisor/clientset/versioned/typed/idp/v1alpha1/fake/fake_githubidentityprovider.go new file mode 100644 index 000000000..305fc8f53 --- /dev/null +++ b/generated/1.26/client/supervisor/clientset/versioned/typed/idp/v1alpha1/fake/fake_githubidentityprovider.go @@ -0,0 +1,129 @@ +// Copyright 2020-2024 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +// Code generated by client-gen. DO NOT EDIT. + +package fake + +import ( + "context" + + v1alpha1 "go.pinniped.dev/generated/1.26/apis/supervisor/idp/v1alpha1" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + labels "k8s.io/apimachinery/pkg/labels" + schema "k8s.io/apimachinery/pkg/runtime/schema" + types "k8s.io/apimachinery/pkg/types" + watch "k8s.io/apimachinery/pkg/watch" + testing "k8s.io/client-go/testing" +) + +// FakeGitHubIdentityProviders implements GitHubIdentityProviderInterface +type FakeGitHubIdentityProviders struct { + Fake *FakeIDPV1alpha1 + ns string +} + +var githubidentityprovidersResource = schema.GroupVersionResource{Group: "idp.supervisor.pinniped.dev", Version: "v1alpha1", Resource: "githubidentityproviders"} + +var githubidentityprovidersKind = schema.GroupVersionKind{Group: "idp.supervisor.pinniped.dev", Version: "v1alpha1", Kind: "GitHubIdentityProvider"} + +// Get takes name of the gitHubIdentityProvider, and returns the corresponding gitHubIdentityProvider object, and an error if there is any. +func (c *FakeGitHubIdentityProviders) Get(ctx context.Context, name string, options v1.GetOptions) (result *v1alpha1.GitHubIdentityProvider, err error) { + obj, err := c.Fake. + Invokes(testing.NewGetAction(githubidentityprovidersResource, c.ns, name), &v1alpha1.GitHubIdentityProvider{}) + + if obj == nil { + return nil, err + } + return obj.(*v1alpha1.GitHubIdentityProvider), err +} + +// List takes label and field selectors, and returns the list of GitHubIdentityProviders that match those selectors. +func (c *FakeGitHubIdentityProviders) List(ctx context.Context, opts v1.ListOptions) (result *v1alpha1.GitHubIdentityProviderList, err error) { + obj, err := c.Fake. + Invokes(testing.NewListAction(githubidentityprovidersResource, githubidentityprovidersKind, c.ns, opts), &v1alpha1.GitHubIdentityProviderList{}) + + if obj == nil { + return nil, err + } + + label, _, _ := testing.ExtractFromListOptions(opts) + if label == nil { + label = labels.Everything() + } + list := &v1alpha1.GitHubIdentityProviderList{ListMeta: obj.(*v1alpha1.GitHubIdentityProviderList).ListMeta} + for _, item := range obj.(*v1alpha1.GitHubIdentityProviderList).Items { + if label.Matches(labels.Set(item.Labels)) { + list.Items = append(list.Items, item) + } + } + return list, err +} + +// Watch returns a watch.Interface that watches the requested gitHubIdentityProviders. +func (c *FakeGitHubIdentityProviders) Watch(ctx context.Context, opts v1.ListOptions) (watch.Interface, error) { + return c.Fake. + InvokesWatch(testing.NewWatchAction(githubidentityprovidersResource, c.ns, opts)) + +} + +// Create takes the representation of a gitHubIdentityProvider and creates it. Returns the server's representation of the gitHubIdentityProvider, and an error, if there is any. +func (c *FakeGitHubIdentityProviders) Create(ctx context.Context, gitHubIdentityProvider *v1alpha1.GitHubIdentityProvider, opts v1.CreateOptions) (result *v1alpha1.GitHubIdentityProvider, err error) { + obj, err := c.Fake. + Invokes(testing.NewCreateAction(githubidentityprovidersResource, c.ns, gitHubIdentityProvider), &v1alpha1.GitHubIdentityProvider{}) + + if obj == nil { + return nil, err + } + return obj.(*v1alpha1.GitHubIdentityProvider), err +} + +// Update takes the representation of a gitHubIdentityProvider and updates it. Returns the server's representation of the gitHubIdentityProvider, and an error, if there is any. +func (c *FakeGitHubIdentityProviders) Update(ctx context.Context, gitHubIdentityProvider *v1alpha1.GitHubIdentityProvider, opts v1.UpdateOptions) (result *v1alpha1.GitHubIdentityProvider, err error) { + obj, err := c.Fake. + Invokes(testing.NewUpdateAction(githubidentityprovidersResource, c.ns, gitHubIdentityProvider), &v1alpha1.GitHubIdentityProvider{}) + + if obj == nil { + return nil, err + } + return obj.(*v1alpha1.GitHubIdentityProvider), err +} + +// UpdateStatus was generated because the type contains a Status member. +// Add a +genclient:noStatus comment above the type to avoid generating UpdateStatus(). +func (c *FakeGitHubIdentityProviders) UpdateStatus(ctx context.Context, gitHubIdentityProvider *v1alpha1.GitHubIdentityProvider, opts v1.UpdateOptions) (*v1alpha1.GitHubIdentityProvider, error) { + obj, err := c.Fake. + Invokes(testing.NewUpdateSubresourceAction(githubidentityprovidersResource, "status", c.ns, gitHubIdentityProvider), &v1alpha1.GitHubIdentityProvider{}) + + if obj == nil { + return nil, err + } + return obj.(*v1alpha1.GitHubIdentityProvider), err +} + +// Delete takes name of the gitHubIdentityProvider and deletes it. Returns an error if one occurs. +func (c *FakeGitHubIdentityProviders) Delete(ctx context.Context, name string, opts v1.DeleteOptions) error { + _, err := c.Fake. + Invokes(testing.NewDeleteActionWithOptions(githubidentityprovidersResource, c.ns, name, opts), &v1alpha1.GitHubIdentityProvider{}) + + return err +} + +// DeleteCollection deletes a collection of objects. +func (c *FakeGitHubIdentityProviders) DeleteCollection(ctx context.Context, opts v1.DeleteOptions, listOpts v1.ListOptions) error { + action := testing.NewDeleteCollectionAction(githubidentityprovidersResource, c.ns, listOpts) + + _, err := c.Fake.Invokes(action, &v1alpha1.GitHubIdentityProviderList{}) + return err +} + +// Patch applies the patch and returns the patched gitHubIdentityProvider. +func (c *FakeGitHubIdentityProviders) Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts v1.PatchOptions, subresources ...string) (result *v1alpha1.GitHubIdentityProvider, err error) { + obj, err := c.Fake. + Invokes(testing.NewPatchSubresourceAction(githubidentityprovidersResource, c.ns, name, pt, data, subresources...), &v1alpha1.GitHubIdentityProvider{}) + + if obj == nil { + return nil, err + } + return obj.(*v1alpha1.GitHubIdentityProvider), err +} diff --git a/generated/1.26/client/supervisor/clientset/versioned/typed/idp/v1alpha1/fake/fake_idp_client.go b/generated/1.26/client/supervisor/clientset/versioned/typed/idp/v1alpha1/fake/fake_idp_client.go index 9f99394f6..3af2f787b 100644 --- a/generated/1.26/client/supervisor/clientset/versioned/typed/idp/v1alpha1/fake/fake_idp_client.go +++ b/generated/1.26/client/supervisor/clientset/versioned/typed/idp/v1alpha1/fake/fake_idp_client.go @@ -19,6 +19,10 @@ func (c *FakeIDPV1alpha1) ActiveDirectoryIdentityProviders(namespace string) v1a return &FakeActiveDirectoryIdentityProviders{c, namespace} } +func (c *FakeIDPV1alpha1) GitHubIdentityProviders(namespace string) v1alpha1.GitHubIdentityProviderInterface { + return &FakeGitHubIdentityProviders{c, namespace} +} + func (c *FakeIDPV1alpha1) LDAPIdentityProviders(namespace string) v1alpha1.LDAPIdentityProviderInterface { return &FakeLDAPIdentityProviders{c, namespace} } diff --git a/generated/1.26/client/supervisor/clientset/versioned/typed/idp/v1alpha1/generated_expansion.go b/generated/1.26/client/supervisor/clientset/versioned/typed/idp/v1alpha1/generated_expansion.go index 79be098d6..a7acc8120 100644 --- a/generated/1.26/client/supervisor/clientset/versioned/typed/idp/v1alpha1/generated_expansion.go +++ b/generated/1.26/client/supervisor/clientset/versioned/typed/idp/v1alpha1/generated_expansion.go @@ -7,6 +7,8 @@ package v1alpha1 type ActiveDirectoryIdentityProviderExpansion interface{} +type GitHubIdentityProviderExpansion interface{} + type LDAPIdentityProviderExpansion interface{} type OIDCIdentityProviderExpansion interface{} diff --git a/generated/1.26/client/supervisor/clientset/versioned/typed/idp/v1alpha1/githubidentityprovider.go b/generated/1.26/client/supervisor/clientset/versioned/typed/idp/v1alpha1/githubidentityprovider.go new file mode 100644 index 000000000..c83e386e4 --- /dev/null +++ b/generated/1.26/client/supervisor/clientset/versioned/typed/idp/v1alpha1/githubidentityprovider.go @@ -0,0 +1,182 @@ +// Copyright 2020-2024 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +// Code generated by client-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + "context" + "time" + + v1alpha1 "go.pinniped.dev/generated/1.26/apis/supervisor/idp/v1alpha1" + scheme "go.pinniped.dev/generated/1.26/client/supervisor/clientset/versioned/scheme" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + types "k8s.io/apimachinery/pkg/types" + watch "k8s.io/apimachinery/pkg/watch" + rest "k8s.io/client-go/rest" +) + +// GitHubIdentityProvidersGetter has a method to return a GitHubIdentityProviderInterface. +// A group's client should implement this interface. +type GitHubIdentityProvidersGetter interface { + GitHubIdentityProviders(namespace string) GitHubIdentityProviderInterface +} + +// GitHubIdentityProviderInterface has methods to work with GitHubIdentityProvider resources. +type GitHubIdentityProviderInterface interface { + Create(ctx context.Context, gitHubIdentityProvider *v1alpha1.GitHubIdentityProvider, opts v1.CreateOptions) (*v1alpha1.GitHubIdentityProvider, error) + Update(ctx context.Context, gitHubIdentityProvider *v1alpha1.GitHubIdentityProvider, opts v1.UpdateOptions) (*v1alpha1.GitHubIdentityProvider, error) + UpdateStatus(ctx context.Context, gitHubIdentityProvider *v1alpha1.GitHubIdentityProvider, opts v1.UpdateOptions) (*v1alpha1.GitHubIdentityProvider, 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) (*v1alpha1.GitHubIdentityProvider, error) + List(ctx context.Context, opts v1.ListOptions) (*v1alpha1.GitHubIdentityProviderList, 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 *v1alpha1.GitHubIdentityProvider, err error) + GitHubIdentityProviderExpansion +} + +// gitHubIdentityProviders implements GitHubIdentityProviderInterface +type gitHubIdentityProviders struct { + client rest.Interface + ns string +} + +// newGitHubIdentityProviders returns a GitHubIdentityProviders +func newGitHubIdentityProviders(c *IDPV1alpha1Client, namespace string) *gitHubIdentityProviders { + return &gitHubIdentityProviders{ + client: c.RESTClient(), + ns: namespace, + } +} + +// Get takes name of the gitHubIdentityProvider, and returns the corresponding gitHubIdentityProvider object, and an error if there is any. +func (c *gitHubIdentityProviders) Get(ctx context.Context, name string, options v1.GetOptions) (result *v1alpha1.GitHubIdentityProvider, err error) { + result = &v1alpha1.GitHubIdentityProvider{} + err = c.client.Get(). + Namespace(c.ns). + Resource("githubidentityproviders"). + Name(name). + VersionedParams(&options, scheme.ParameterCodec). + Do(ctx). + Into(result) + return +} + +// List takes label and field selectors, and returns the list of GitHubIdentityProviders that match those selectors. +func (c *gitHubIdentityProviders) List(ctx context.Context, opts v1.ListOptions) (result *v1alpha1.GitHubIdentityProviderList, err error) { + var timeout time.Duration + if opts.TimeoutSeconds != nil { + timeout = time.Duration(*opts.TimeoutSeconds) * time.Second + } + result = &v1alpha1.GitHubIdentityProviderList{} + err = c.client.Get(). + Namespace(c.ns). + Resource("githubidentityproviders"). + VersionedParams(&opts, scheme.ParameterCodec). + Timeout(timeout). + Do(ctx). + Into(result) + return +} + +// Watch returns a watch.Interface that watches the requested gitHubIdentityProviders. +func (c *gitHubIdentityProviders) Watch(ctx context.Context, opts v1.ListOptions) (watch.Interface, error) { + var timeout time.Duration + if opts.TimeoutSeconds != nil { + timeout = time.Duration(*opts.TimeoutSeconds) * time.Second + } + opts.Watch = true + return c.client.Get(). + Namespace(c.ns). + Resource("githubidentityproviders"). + VersionedParams(&opts, scheme.ParameterCodec). + Timeout(timeout). + Watch(ctx) +} + +// Create takes the representation of a gitHubIdentityProvider and creates it. Returns the server's representation of the gitHubIdentityProvider, and an error, if there is any. +func (c *gitHubIdentityProviders) Create(ctx context.Context, gitHubIdentityProvider *v1alpha1.GitHubIdentityProvider, opts v1.CreateOptions) (result *v1alpha1.GitHubIdentityProvider, err error) { + result = &v1alpha1.GitHubIdentityProvider{} + err = c.client.Post(). + Namespace(c.ns). + Resource("githubidentityproviders"). + VersionedParams(&opts, scheme.ParameterCodec). + Body(gitHubIdentityProvider). + Do(ctx). + Into(result) + return +} + +// Update takes the representation of a gitHubIdentityProvider and updates it. Returns the server's representation of the gitHubIdentityProvider, and an error, if there is any. +func (c *gitHubIdentityProviders) Update(ctx context.Context, gitHubIdentityProvider *v1alpha1.GitHubIdentityProvider, opts v1.UpdateOptions) (result *v1alpha1.GitHubIdentityProvider, err error) { + result = &v1alpha1.GitHubIdentityProvider{} + err = c.client.Put(). + Namespace(c.ns). + Resource("githubidentityproviders"). + Name(gitHubIdentityProvider.Name). + VersionedParams(&opts, scheme.ParameterCodec). + Body(gitHubIdentityProvider). + Do(ctx). + Into(result) + return +} + +// UpdateStatus was generated because the type contains a Status member. +// Add a +genclient:noStatus comment above the type to avoid generating UpdateStatus(). +func (c *gitHubIdentityProviders) UpdateStatus(ctx context.Context, gitHubIdentityProvider *v1alpha1.GitHubIdentityProvider, opts v1.UpdateOptions) (result *v1alpha1.GitHubIdentityProvider, err error) { + result = &v1alpha1.GitHubIdentityProvider{} + err = c.client.Put(). + Namespace(c.ns). + Resource("githubidentityproviders"). + Name(gitHubIdentityProvider.Name). + SubResource("status"). + VersionedParams(&opts, scheme.ParameterCodec). + Body(gitHubIdentityProvider). + Do(ctx). + Into(result) + return +} + +// Delete takes name of the gitHubIdentityProvider and deletes it. Returns an error if one occurs. +func (c *gitHubIdentityProviders) Delete(ctx context.Context, name string, opts v1.DeleteOptions) error { + return c.client.Delete(). + Namespace(c.ns). + Resource("githubidentityproviders"). + Name(name). + Body(&opts). + Do(ctx). + Error() +} + +// DeleteCollection deletes a collection of objects. +func (c *gitHubIdentityProviders) DeleteCollection(ctx context.Context, opts v1.DeleteOptions, listOpts v1.ListOptions) error { + var timeout time.Duration + if listOpts.TimeoutSeconds != nil { + timeout = time.Duration(*listOpts.TimeoutSeconds) * time.Second + } + return c.client.Delete(). + Namespace(c.ns). + Resource("githubidentityproviders"). + VersionedParams(&listOpts, scheme.ParameterCodec). + Timeout(timeout). + Body(&opts). + Do(ctx). + Error() +} + +// Patch applies the patch and returns the patched gitHubIdentityProvider. +func (c *gitHubIdentityProviders) Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts v1.PatchOptions, subresources ...string) (result *v1alpha1.GitHubIdentityProvider, err error) { + result = &v1alpha1.GitHubIdentityProvider{} + err = c.client.Patch(pt). + Namespace(c.ns). + Resource("githubidentityproviders"). + Name(name). + SubResource(subresources...). + VersionedParams(&opts, scheme.ParameterCodec). + Body(data). + Do(ctx). + Into(result) + return +} diff --git a/generated/1.26/client/supervisor/clientset/versioned/typed/idp/v1alpha1/idp_client.go b/generated/1.26/client/supervisor/clientset/versioned/typed/idp/v1alpha1/idp_client.go index d6c8bd42b..a1e8d6ce3 100644 --- a/generated/1.26/client/supervisor/clientset/versioned/typed/idp/v1alpha1/idp_client.go +++ b/generated/1.26/client/supervisor/clientset/versioned/typed/idp/v1alpha1/idp_client.go @@ -16,6 +16,7 @@ import ( type IDPV1alpha1Interface interface { RESTClient() rest.Interface ActiveDirectoryIdentityProvidersGetter + GitHubIdentityProvidersGetter LDAPIdentityProvidersGetter OIDCIdentityProvidersGetter } @@ -29,6 +30,10 @@ func (c *IDPV1alpha1Client) ActiveDirectoryIdentityProviders(namespace string) A return newActiveDirectoryIdentityProviders(c, namespace) } +func (c *IDPV1alpha1Client) GitHubIdentityProviders(namespace string) GitHubIdentityProviderInterface { + return newGitHubIdentityProviders(c, namespace) +} + func (c *IDPV1alpha1Client) LDAPIdentityProviders(namespace string) LDAPIdentityProviderInterface { return newLDAPIdentityProviders(c, namespace) } diff --git a/generated/1.26/client/supervisor/informers/externalversions/generic.go b/generated/1.26/client/supervisor/informers/externalversions/generic.go index a9c2b1bc4..fa7dc1695 100644 --- a/generated/1.26/client/supervisor/informers/externalversions/generic.go +++ b/generated/1.26/client/supervisor/informers/externalversions/generic.go @@ -49,6 +49,8 @@ func (f *sharedInformerFactory) ForResource(resource schema.GroupVersionResource // Group=idp.supervisor.pinniped.dev, Version=v1alpha1 case idpv1alpha1.SchemeGroupVersion.WithResource("activedirectoryidentityproviders"): return &genericInformer{resource: resource.GroupResource(), informer: f.IDP().V1alpha1().ActiveDirectoryIdentityProviders().Informer()}, nil + case idpv1alpha1.SchemeGroupVersion.WithResource("githubidentityproviders"): + return &genericInformer{resource: resource.GroupResource(), informer: f.IDP().V1alpha1().GitHubIdentityProviders().Informer()}, nil case idpv1alpha1.SchemeGroupVersion.WithResource("ldapidentityproviders"): return &genericInformer{resource: resource.GroupResource(), informer: f.IDP().V1alpha1().LDAPIdentityProviders().Informer()}, nil case idpv1alpha1.SchemeGroupVersion.WithResource("oidcidentityproviders"): diff --git a/generated/1.26/client/supervisor/informers/externalversions/idp/v1alpha1/githubidentityprovider.go b/generated/1.26/client/supervisor/informers/externalversions/idp/v1alpha1/githubidentityprovider.go new file mode 100644 index 000000000..f86968424 --- /dev/null +++ b/generated/1.26/client/supervisor/informers/externalversions/idp/v1alpha1/githubidentityprovider.go @@ -0,0 +1,77 @@ +// Copyright 2020-2024 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +// Code generated by informer-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + "context" + time "time" + + idpv1alpha1 "go.pinniped.dev/generated/1.26/apis/supervisor/idp/v1alpha1" + versioned "go.pinniped.dev/generated/1.26/client/supervisor/clientset/versioned" + internalinterfaces "go.pinniped.dev/generated/1.26/client/supervisor/informers/externalversions/internalinterfaces" + v1alpha1 "go.pinniped.dev/generated/1.26/client/supervisor/listers/idp/v1alpha1" + 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" +) + +// GitHubIdentityProviderInformer provides access to a shared informer and lister for +// GitHubIdentityProviders. +type GitHubIdentityProviderInformer interface { + Informer() cache.SharedIndexInformer + Lister() v1alpha1.GitHubIdentityProviderLister +} + +type gitHubIdentityProviderInformer struct { + factory internalinterfaces.SharedInformerFactory + tweakListOptions internalinterfaces.TweakListOptionsFunc + namespace string +} + +// NewGitHubIdentityProviderInformer constructs a new informer for GitHubIdentityProvider 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 NewGitHubIdentityProviderInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers) cache.SharedIndexInformer { + return NewFilteredGitHubIdentityProviderInformer(client, namespace, resyncPeriod, indexers, nil) +} + +// NewFilteredGitHubIdentityProviderInformer constructs a new informer for GitHubIdentityProvider 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 NewFilteredGitHubIdentityProviderInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers, tweakListOptions internalinterfaces.TweakListOptionsFunc) cache.SharedIndexInformer { + return cache.NewSharedIndexInformer( + &cache.ListWatch{ + ListFunc: func(options v1.ListOptions) (runtime.Object, error) { + if tweakListOptions != nil { + tweakListOptions(&options) + } + return client.IDPV1alpha1().GitHubIdentityProviders(namespace).List(context.TODO(), options) + }, + WatchFunc: func(options v1.ListOptions) (watch.Interface, error) { + if tweakListOptions != nil { + tweakListOptions(&options) + } + return client.IDPV1alpha1().GitHubIdentityProviders(namespace).Watch(context.TODO(), options) + }, + }, + &idpv1alpha1.GitHubIdentityProvider{}, + resyncPeriod, + indexers, + ) +} + +func (f *gitHubIdentityProviderInformer) defaultInformer(client versioned.Interface, resyncPeriod time.Duration) cache.SharedIndexInformer { + return NewFilteredGitHubIdentityProviderInformer(client, f.namespace, resyncPeriod, cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc}, f.tweakListOptions) +} + +func (f *gitHubIdentityProviderInformer) Informer() cache.SharedIndexInformer { + return f.factory.InformerFor(&idpv1alpha1.GitHubIdentityProvider{}, f.defaultInformer) +} + +func (f *gitHubIdentityProviderInformer) Lister() v1alpha1.GitHubIdentityProviderLister { + return v1alpha1.NewGitHubIdentityProviderLister(f.Informer().GetIndexer()) +} diff --git a/generated/1.26/client/supervisor/informers/externalversions/idp/v1alpha1/interface.go b/generated/1.26/client/supervisor/informers/externalversions/idp/v1alpha1/interface.go index 8edaafa4a..1c89e4b1d 100644 --- a/generated/1.26/client/supervisor/informers/externalversions/idp/v1alpha1/interface.go +++ b/generated/1.26/client/supervisor/informers/externalversions/idp/v1alpha1/interface.go @@ -13,6 +13,8 @@ import ( type Interface interface { // ActiveDirectoryIdentityProviders returns a ActiveDirectoryIdentityProviderInformer. ActiveDirectoryIdentityProviders() ActiveDirectoryIdentityProviderInformer + // GitHubIdentityProviders returns a GitHubIdentityProviderInformer. + GitHubIdentityProviders() GitHubIdentityProviderInformer // LDAPIdentityProviders returns a LDAPIdentityProviderInformer. LDAPIdentityProviders() LDAPIdentityProviderInformer // OIDCIdentityProviders returns a OIDCIdentityProviderInformer. @@ -35,6 +37,11 @@ func (v *version) ActiveDirectoryIdentityProviders() ActiveDirectoryIdentityProv return &activeDirectoryIdentityProviderInformer{factory: v.factory, namespace: v.namespace, tweakListOptions: v.tweakListOptions} } +// GitHubIdentityProviders returns a GitHubIdentityProviderInformer. +func (v *version) GitHubIdentityProviders() GitHubIdentityProviderInformer { + return &gitHubIdentityProviderInformer{factory: v.factory, namespace: v.namespace, tweakListOptions: v.tweakListOptions} +} + // LDAPIdentityProviders returns a LDAPIdentityProviderInformer. func (v *version) LDAPIdentityProviders() LDAPIdentityProviderInformer { return &lDAPIdentityProviderInformer{factory: v.factory, namespace: v.namespace, tweakListOptions: v.tweakListOptions} diff --git a/generated/1.26/client/supervisor/listers/idp/v1alpha1/expansion_generated.go b/generated/1.26/client/supervisor/listers/idp/v1alpha1/expansion_generated.go index b491a9e8d..470c06abb 100644 --- a/generated/1.26/client/supervisor/listers/idp/v1alpha1/expansion_generated.go +++ b/generated/1.26/client/supervisor/listers/idp/v1alpha1/expansion_generated.go @@ -13,6 +13,14 @@ type ActiveDirectoryIdentityProviderListerExpansion interface{} // ActiveDirectoryIdentityProviderNamespaceLister. type ActiveDirectoryIdentityProviderNamespaceListerExpansion interface{} +// GitHubIdentityProviderListerExpansion allows custom methods to be added to +// GitHubIdentityProviderLister. +type GitHubIdentityProviderListerExpansion interface{} + +// GitHubIdentityProviderNamespaceListerExpansion allows custom methods to be added to +// GitHubIdentityProviderNamespaceLister. +type GitHubIdentityProviderNamespaceListerExpansion interface{} + // LDAPIdentityProviderListerExpansion allows custom methods to be added to // LDAPIdentityProviderLister. type LDAPIdentityProviderListerExpansion interface{} diff --git a/generated/1.26/client/supervisor/listers/idp/v1alpha1/githubidentityprovider.go b/generated/1.26/client/supervisor/listers/idp/v1alpha1/githubidentityprovider.go new file mode 100644 index 000000000..a11c30dc5 --- /dev/null +++ b/generated/1.26/client/supervisor/listers/idp/v1alpha1/githubidentityprovider.go @@ -0,0 +1,86 @@ +// Copyright 2020-2024 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +// Code generated by lister-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + v1alpha1 "go.pinniped.dev/generated/1.26/apis/supervisor/idp/v1alpha1" + "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/client-go/tools/cache" +) + +// GitHubIdentityProviderLister helps list GitHubIdentityProviders. +// All objects returned here must be treated as read-only. +type GitHubIdentityProviderLister interface { + // List lists all GitHubIdentityProviders in the indexer. + // Objects returned here must be treated as read-only. + List(selector labels.Selector) (ret []*v1alpha1.GitHubIdentityProvider, err error) + // GitHubIdentityProviders returns an object that can list and get GitHubIdentityProviders. + GitHubIdentityProviders(namespace string) GitHubIdentityProviderNamespaceLister + GitHubIdentityProviderListerExpansion +} + +// gitHubIdentityProviderLister implements the GitHubIdentityProviderLister interface. +type gitHubIdentityProviderLister struct { + indexer cache.Indexer +} + +// NewGitHubIdentityProviderLister returns a new GitHubIdentityProviderLister. +func NewGitHubIdentityProviderLister(indexer cache.Indexer) GitHubIdentityProviderLister { + return &gitHubIdentityProviderLister{indexer: indexer} +} + +// List lists all GitHubIdentityProviders in the indexer. +func (s *gitHubIdentityProviderLister) List(selector labels.Selector) (ret []*v1alpha1.GitHubIdentityProvider, err error) { + err = cache.ListAll(s.indexer, selector, func(m interface{}) { + ret = append(ret, m.(*v1alpha1.GitHubIdentityProvider)) + }) + return ret, err +} + +// GitHubIdentityProviders returns an object that can list and get GitHubIdentityProviders. +func (s *gitHubIdentityProviderLister) GitHubIdentityProviders(namespace string) GitHubIdentityProviderNamespaceLister { + return gitHubIdentityProviderNamespaceLister{indexer: s.indexer, namespace: namespace} +} + +// GitHubIdentityProviderNamespaceLister helps list and get GitHubIdentityProviders. +// All objects returned here must be treated as read-only. +type GitHubIdentityProviderNamespaceLister interface { + // List lists all GitHubIdentityProviders in the indexer for a given namespace. + // Objects returned here must be treated as read-only. + List(selector labels.Selector) (ret []*v1alpha1.GitHubIdentityProvider, err error) + // Get retrieves the GitHubIdentityProvider from the indexer for a given namespace and name. + // Objects returned here must be treated as read-only. + Get(name string) (*v1alpha1.GitHubIdentityProvider, error) + GitHubIdentityProviderNamespaceListerExpansion +} + +// gitHubIdentityProviderNamespaceLister implements the GitHubIdentityProviderNamespaceLister +// interface. +type gitHubIdentityProviderNamespaceLister struct { + indexer cache.Indexer + namespace string +} + +// List lists all GitHubIdentityProviders in the indexer for a given namespace. +func (s gitHubIdentityProviderNamespaceLister) List(selector labels.Selector) (ret []*v1alpha1.GitHubIdentityProvider, err error) { + err = cache.ListAllByNamespace(s.indexer, s.namespace, selector, func(m interface{}) { + ret = append(ret, m.(*v1alpha1.GitHubIdentityProvider)) + }) + return ret, err +} + +// Get retrieves the GitHubIdentityProvider from the indexer for a given namespace and name. +func (s gitHubIdentityProviderNamespaceLister) Get(name string) (*v1alpha1.GitHubIdentityProvider, error) { + obj, exists, err := s.indexer.GetByKey(s.namespace + "/" + name) + if err != nil { + return nil, err + } + if !exists { + return nil, errors.NewNotFound(v1alpha1.Resource("githubidentityprovider"), name) + } + return obj.(*v1alpha1.GitHubIdentityProvider), nil +} diff --git a/generated/1.26/crds/authentication.concierge.pinniped.dev_jwtauthenticators.yaml b/generated/1.26/crds/authentication.concierge.pinniped.dev_jwtauthenticators.yaml index b464cfa2b..d59fcb783 100644 --- a/generated/1.26/crds/authentication.concierge.pinniped.dev_jwtauthenticators.yaml +++ b/generated/1.26/crds/authentication.concierge.pinniped.dev_jwtauthenticators.yaml @@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - controller-gen.kubebuilder.io/version: v0.14.0 + controller-gen.kubebuilder.io/version: v0.15.0 name: jwtauthenticators.authentication.concierge.pinniped.dev spec: group: authentication.concierge.pinniped.dev diff --git a/generated/1.26/crds/authentication.concierge.pinniped.dev_webhookauthenticators.yaml b/generated/1.26/crds/authentication.concierge.pinniped.dev_webhookauthenticators.yaml index e8cbc6790..4ccd53770 100644 --- a/generated/1.26/crds/authentication.concierge.pinniped.dev_webhookauthenticators.yaml +++ b/generated/1.26/crds/authentication.concierge.pinniped.dev_webhookauthenticators.yaml @@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - controller-gen.kubebuilder.io/version: v0.14.0 + controller-gen.kubebuilder.io/version: v0.15.0 name: webhookauthenticators.authentication.concierge.pinniped.dev spec: group: authentication.concierge.pinniped.dev diff --git a/generated/1.26/crds/config.concierge.pinniped.dev_credentialissuers.yaml b/generated/1.26/crds/config.concierge.pinniped.dev_credentialissuers.yaml index 8d77a6665..26fed7376 100644 --- a/generated/1.26/crds/config.concierge.pinniped.dev_credentialissuers.yaml +++ b/generated/1.26/crds/config.concierge.pinniped.dev_credentialissuers.yaml @@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - controller-gen.kubebuilder.io/version: v0.14.0 + controller-gen.kubebuilder.io/version: v0.15.0 name: credentialissuers.config.concierge.pinniped.dev spec: group: config.concierge.pinniped.dev diff --git a/generated/1.26/crds/config.supervisor.pinniped.dev_federationdomains.yaml b/generated/1.26/crds/config.supervisor.pinniped.dev_federationdomains.yaml index 7bf231443..d43c7406d 100644 --- a/generated/1.26/crds/config.supervisor.pinniped.dev_federationdomains.yaml +++ b/generated/1.26/crds/config.supervisor.pinniped.dev_federationdomains.yaml @@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - controller-gen.kubebuilder.io/version: v0.14.0 + controller-gen.kubebuilder.io/version: v0.15.0 name: federationdomains.config.supervisor.pinniped.dev spec: group: config.supervisor.pinniped.dev diff --git a/generated/1.26/crds/config.supervisor.pinniped.dev_oidcclients.yaml b/generated/1.26/crds/config.supervisor.pinniped.dev_oidcclients.yaml index d33b31bf1..c44ecbf1e 100644 --- a/generated/1.26/crds/config.supervisor.pinniped.dev_oidcclients.yaml +++ b/generated/1.26/crds/config.supervisor.pinniped.dev_oidcclients.yaml @@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - controller-gen.kubebuilder.io/version: v0.14.0 + controller-gen.kubebuilder.io/version: v0.15.0 name: oidcclients.config.supervisor.pinniped.dev spec: group: config.supervisor.pinniped.dev diff --git a/generated/1.26/crds/idp.supervisor.pinniped.dev_activedirectoryidentityproviders.yaml b/generated/1.26/crds/idp.supervisor.pinniped.dev_activedirectoryidentityproviders.yaml index 414ac4f51..062251102 100644 --- a/generated/1.26/crds/idp.supervisor.pinniped.dev_activedirectoryidentityproviders.yaml +++ b/generated/1.26/crds/idp.supervisor.pinniped.dev_activedirectoryidentityproviders.yaml @@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - controller-gen.kubebuilder.io/version: v0.14.0 + controller-gen.kubebuilder.io/version: v0.15.0 name: activedirectoryidentityproviders.idp.supervisor.pinniped.dev spec: group: idp.supervisor.pinniped.dev diff --git a/generated/1.26/crds/idp.supervisor.pinniped.dev_githubidentityproviders.yaml b/generated/1.26/crds/idp.supervisor.pinniped.dev_githubidentityproviders.yaml new file mode 100644 index 000000000..f93108700 --- /dev/null +++ b/generated/1.26/crds/idp.supervisor.pinniped.dev_githubidentityproviders.yaml @@ -0,0 +1,326 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.15.0 + name: githubidentityproviders.idp.supervisor.pinniped.dev +spec: + group: idp.supervisor.pinniped.dev + names: + categories: + - pinniped + - pinniped-idp + - pinniped-idps + kind: GitHubIdentityProvider + listKind: GitHubIdentityProviderList + plural: githubidentityproviders + singular: githubidentityprovider + scope: Namespaced + versions: + - additionalPrinterColumns: + - jsonPath: .spec.githubAPI.host + name: Host + type: string + - jsonPath: .status.phase + name: Status + type: string + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1alpha1 + schema: + openAPIV3Schema: + description: |- + GitHubIdentityProvider describes the configuration of an upstream GitHub identity provider. + This upstream provider can be configured with either a GitHub App or a GitHub OAuth2 App. + + + Right now, only web-based logins are supported, for both the pinniped-cli client and clients configured + as OIDCClients. + 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: + description: Spec for configuring the identity provider. + properties: + allowAuthentication: + description: AllowAuthentication allows customization of who can authenticate + using this IDP and how. + properties: + organizations: + description: Organizations allows customization of which organizations + can authenticate using this IDP. + properties: + allowed: + description: |- + Allowed, when specified, indicates that only users with membership in at least one of the listed + GitHub organizations may log in. In addition, the group membership presented to Kubernetes will only include + teams within the listed GitHub organizations. Additional login rules or group filtering can optionally be + provided as policy expression on any Pinniped Supervisor FederationDomain that includes this IDP. + + + The configured GitHub App or GitHub OAuth App must be allowed to see membership in the listed organizations, + otherwise Pinniped will not be aware that the user belongs to the listed organization or any teams + within that organization. + + + If no organizations are listed, you must set organizations: AllGitHubUsers. + items: + type: string + maxItems: 64 + type: array + x-kubernetes-list-type: set + policy: + default: OnlyUsersFromAllowedOrganizations + description: |- + Policy must be set to "AllGitHubUsers" if allowed is empty. + + + This field only exists to ensure that Pinniped administrators are aware that an empty list of + allowedOrganizations means all GitHub users are allowed to log in. + enum: + - OnlyUsersFromAllowedOrganizations + - AllGitHubUsers + type: string + type: object + x-kubernetes-validations: + - message: spec.allowAuthentication.organizations.policy must + be 'OnlyUsersFromAllowedOrganizations' when spec.allowAuthentication.organizations.allowed + has organizations listed + rule: '!(has(self.allowed) && size(self.allowed) > 0 && self.policy + == ''AllGitHubUsers'')' + - message: spec.allowAuthentication.organizations.policy must + be 'AllGitHubUsers' when spec.allowAuthentication.organizations.allowed + is empty + rule: '!((!has(self.allowed) || size(self.allowed) == 0) && + self.policy == ''OnlyUsersFromAllowedOrganizations'')' + required: + - organizations + type: object + claims: + default: {} + description: Claims allows customization of the username and groups + claims. + properties: + groups: + default: slug + description: |- + Groups configures which property of the GitHub team record shall determine the group names in Kubernetes. + + + Can be either "name" or "slug". Defaults to "slug". + + + GitHub team names can contain upper and lower case characters, whitespace, and punctuation (e.g. "Kube admins!"). + + + GitHub team slugs are lower case alphanumeric characters and may contain dashes and underscores (e.g. "kube-admins"). + + + Group names as presented to Kubernetes will always be prefixed by the GitHub organization name followed by a + forward slash (e.g. "my-org/my-team"). GitHub organization login names can only contain alphanumeric characters + or single hyphens, so the first forward slash `/` will be the separator between the organization login name and + the team name or slug. + + + If desired, an admin could configure identity transformation expressions on the Pinniped Supervisor's + FederationDomain to further customize how these group names are presented to Kubernetes. + + + See the response schema for + [List teams for the authenticated user](https://docs.github.com/en/rest/teams/teams?apiVersion=2022-11-28#list-teams-for-the-authenticated-user). + enum: + - name + - slug + type: string + username: + default: login:id + description: |- + Username configures which property of the GitHub user record shall determine the username in Kubernetes. + + + Can be either "id", "login", or "login:id". Defaults to "login:id". + + + GitHub's user login attributes can only contain alphanumeric characters and non-repeating hyphens, + and may not start or end with hyphens. GitHub users are allowed to change their login name, + although it is inconvenient. If a GitHub user changed their login name from "foo" to "bar", + then a second user might change their name from "baz" to "foo" in order to take the old + username of the first user. For this reason, it is not as safe to make authorization decisions + based only on the user's login attribute. + + + If desired, an admin could configure identity transformation expressions on the Pinniped Supervisor's + FederationDomain to further customize how these usernames are presented to Kubernetes. + + + Defaults to "login:id", which is the user login attribute, followed by a colon, followed by the unique and + unchanging integer ID number attribute. This blends human-readable login names with the unchanging ID value + from GitHub. Colons are not allowed in GitHub login attributes or ID numbers, so this is a reasonable + choice to concatenate the two values. + + + See the response schema for + [Get the authenticated user](https://docs.github.com/en/rest/users/users?apiVersion=2022-11-28#get-the-authenticated-user). + enum: + - id + - login + - login:id + type: string + type: object + client: + description: Client identifies the secret with credentials for a GitHub + App or GitHub OAuth2 App (a GitHub client). + properties: + secretName: + description: |- + SecretName contains the name of a namespace-local Secret object that provides the clientID and + clientSecret for an GitHub App or GitHub OAuth2 client. + + + This secret must be of type "secrets.pinniped.dev/github-client" with keys "clientID" and "clientSecret". + minLength: 1 + type: string + required: + - secretName + type: object + githubAPI: + default: {} + description: GitHubAPI allows configuration for GitHub Enterprise + Server + properties: + host: + default: github.com + description: |- + Host is required only for GitHub Enterprise Server. + Defaults to using GitHub's public API ("github.com"). + Do not specify a protocol or scheme since "https://" will always be used. + Port is optional. Do not specify a path, query, fragment, or userinfo. + Only domain name or IP address, subdomains (optional), and port (optional). + IPv4 and IPv6 are supported. If using an IPv6 address with a port, you must enclose the IPv6 address + in square brackets. Example: "[::1]:443". + minLength: 1 + type: string + tls: + description: TLS configuration for GitHub Enterprise Server. + properties: + certificateAuthorityData: + description: X.509 Certificate Authority (base64-encoded PEM + bundle). If omitted, a default set of system roots will + be trusted. + type: string + type: object + type: object + required: + - allowAuthentication + - client + type: object + status: + description: Status of the identity provider. + properties: + conditions: + description: Conditions represents the observations of an identity + provider's current state. + items: + description: "Condition contains details for one aspect of the current + state of this API Resource.\n---\nThis struct is intended for + direct use as an array at the field path .status.conditions. For + example,\n\n\n\ttype FooStatus struct{\n\t // Represents the + observations of a foo's current state.\n\t // Known .status.conditions.type + are: \"Available\", \"Progressing\", and \"Degraded\"\n\t // + +patchMergeKey=type\n\t // +patchStrategy=merge\n\t // +listType=map\n\t + \ // +listMapKey=type\n\t Conditions []metav1.Condition `json:\"conditions,omitempty\" + patchStrategy:\"merge\" patchMergeKey:\"type\" protobuf:\"bytes,1,rep,name=conditions\"`\n\n\n\t + \ // other fields\n\t}" + properties: + lastTransitionTime: + description: |- + lastTransitionTime is the last time the condition transitioned from one status to another. + This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: |- + message is a human readable message indicating details about the transition. + This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: |- + observedGeneration represents the .metadata.generation that the condition was set based upon. + For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date + with respect to the current state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: |- + reason contains a programmatic identifier indicating the reason for the condition's last transition. + Producers of specific condition types may define expected values and meanings for this field, + and whether the values are considered a guaranteed API. + The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: |- + type of condition in CamelCase or in foo.example.com/CamelCase. + --- + Many .condition.type values are consistent across resources like Available, but because arbitrary conditions can be + useful (see .node.status.conditions), the ability to deconflict is important. + The regex it matches is (dns1123SubdomainFmt/)?(qualifiedNameFmt) + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + x-kubernetes-list-map-keys: + - type + x-kubernetes-list-type: map + phase: + default: Pending + description: Phase summarizes the overall status of the GitHubIdentityProvider. + enum: + - Pending + - Ready + - Error + type: string + type: object + required: + - spec + type: object + served: true + storage: true + subresources: + status: {} diff --git a/generated/1.26/crds/idp.supervisor.pinniped.dev_ldapidentityproviders.yaml b/generated/1.26/crds/idp.supervisor.pinniped.dev_ldapidentityproviders.yaml index f718e93aa..711e9a754 100644 --- a/generated/1.26/crds/idp.supervisor.pinniped.dev_ldapidentityproviders.yaml +++ b/generated/1.26/crds/idp.supervisor.pinniped.dev_ldapidentityproviders.yaml @@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - controller-gen.kubebuilder.io/version: v0.14.0 + controller-gen.kubebuilder.io/version: v0.15.0 name: ldapidentityproviders.idp.supervisor.pinniped.dev spec: group: idp.supervisor.pinniped.dev diff --git a/generated/1.26/crds/idp.supervisor.pinniped.dev_oidcidentityproviders.yaml b/generated/1.26/crds/idp.supervisor.pinniped.dev_oidcidentityproviders.yaml index 486665605..acfca1573 100644 --- a/generated/1.26/crds/idp.supervisor.pinniped.dev_oidcidentityproviders.yaml +++ b/generated/1.26/crds/idp.supervisor.pinniped.dev_oidcidentityproviders.yaml @@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - controller-gen.kubebuilder.io/version: v0.14.0 + controller-gen.kubebuilder.io/version: v0.15.0 name: oidcidentityproviders.idp.supervisor.pinniped.dev spec: group: idp.supervisor.pinniped.dev diff --git a/generated/1.27/README.adoc b/generated/1.27/README.adoc index 288bf3e6b..3fd81787a 100644 --- a/generated/1.27/README.adoc +++ b/generated/1.27/README.adoc @@ -1645,6 +1645,285 @@ Optional, when empty this defaults to "objectGUID". + |=== +[id="{anchor_prefix}-go-pinniped-dev-generated-1-27-apis-supervisor-idp-v1alpha1-githubapiconfig"] +==== GitHubAPIConfig + +GitHubAPIConfig allows configuration for GitHub Enterprise Server + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-27-apis-supervisor-idp-v1alpha1-githubidentityproviderspec[$$GitHubIdentityProviderSpec$$] +**** + +[cols="25a,75a", options="header"] +|=== +| Field | Description +| *`host`* __string__ | Host is required only for GitHub Enterprise Server. + +Defaults to using GitHub's public API ("github.com"). + +Do not specify a protocol or scheme since "https://" will always be used. + +Port is optional. Do not specify a path, query, fragment, or userinfo. + +Only domain name or IP address, subdomains (optional), and port (optional). + +IPv4 and IPv6 are supported. If using an IPv6 address with a port, you must enclose the IPv6 address + +in square brackets. Example: "[::1]:443". + +| *`tls`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-27-apis-supervisor-idp-v1alpha1-tlsspec[$$TLSSpec$$]__ | TLS configuration for GitHub Enterprise Server. + +|=== + + +[id="{anchor_prefix}-go-pinniped-dev-generated-1-27-apis-supervisor-idp-v1alpha1-githuballowauthenticationspec"] +==== GitHubAllowAuthenticationSpec + +GitHubAllowAuthenticationSpec allows customization of who can authenticate using this IDP and how. + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-27-apis-supervisor-idp-v1alpha1-githubidentityproviderspec[$$GitHubIdentityProviderSpec$$] +**** + +[cols="25a,75a", options="header"] +|=== +| Field | Description +| *`organizations`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-27-apis-supervisor-idp-v1alpha1-githuborganizationsspec[$$GitHubOrganizationsSpec$$]__ | Organizations allows customization of which organizations can authenticate using this IDP. + +|=== + + +[id="{anchor_prefix}-go-pinniped-dev-generated-1-27-apis-supervisor-idp-v1alpha1-githuballowedauthorganizationspolicy"] +==== GitHubAllowedAuthOrganizationsPolicy (string) + + + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-27-apis-supervisor-idp-v1alpha1-githuborganizationsspec[$$GitHubOrganizationsSpec$$] +**** + + + +[id="{anchor_prefix}-go-pinniped-dev-generated-1-27-apis-supervisor-idp-v1alpha1-githubclaims"] +==== GitHubClaims + +GitHubClaims allows customization of the username and groups claims. + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-27-apis-supervisor-idp-v1alpha1-githubidentityproviderspec[$$GitHubIdentityProviderSpec$$] +**** + +[cols="25a,75a", options="header"] +|=== +| Field | Description +| *`username`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-27-apis-supervisor-idp-v1alpha1-githubusernameattribute[$$GitHubUsernameAttribute$$]__ | Username configures which property of the GitHub user record shall determine the username in Kubernetes. + + + +Can be either "id", "login", or "login:id". Defaults to "login:id". + + + +GitHub's user login attributes can only contain alphanumeric characters and non-repeating hyphens, + +and may not start or end with hyphens. GitHub users are allowed to change their login name, + +although it is inconvenient. If a GitHub user changed their login name from "foo" to "bar", + +then a second user might change their name from "baz" to "foo" in order to take the old + +username of the first user. For this reason, it is not as safe to make authorization decisions + +based only on the user's login attribute. + + + +If desired, an admin could configure identity transformation expressions on the Pinniped Supervisor's + +FederationDomain to further customize how these usernames are presented to Kubernetes. + + + +Defaults to "login:id", which is the user login attribute, followed by a colon, followed by the unique and + +unchanging integer ID number attribute. This blends human-readable login names with the unchanging ID value + +from GitHub. Colons are not allowed in GitHub login attributes or ID numbers, so this is a reasonable + +choice to concatenate the two values. + + + +See the response schema for + +[Get the authenticated user](https://docs.github.com/en/rest/users/users?apiVersion=2022-11-28#get-the-authenticated-user). + +| *`groups`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-27-apis-supervisor-idp-v1alpha1-githubgroupnameattribute[$$GitHubGroupNameAttribute$$]__ | Groups configures which property of the GitHub team record shall determine the group names in Kubernetes. + + + +Can be either "name" or "slug". Defaults to "slug". + + + +GitHub team names can contain upper and lower case characters, whitespace, and punctuation (e.g. "Kube admins!"). + + + +GitHub team slugs are lower case alphanumeric characters and may contain dashes and underscores (e.g. "kube-admins"). + + + +Group names as presented to Kubernetes will always be prefixed by the GitHub organization name followed by a + +forward slash (e.g. "my-org/my-team"). GitHub organization login names can only contain alphanumeric characters + +or single hyphens, so the first forward slash `/` will be the separator between the organization login name and + +the team name or slug. + + + +If desired, an admin could configure identity transformation expressions on the Pinniped Supervisor's + +FederationDomain to further customize how these group names are presented to Kubernetes. + + + +See the response schema for + +[List teams for the authenticated user](https://docs.github.com/en/rest/teams/teams?apiVersion=2022-11-28#list-teams-for-the-authenticated-user). + +|=== + + +[id="{anchor_prefix}-go-pinniped-dev-generated-1-27-apis-supervisor-idp-v1alpha1-githubclientspec"] +==== GitHubClientSpec + +GitHubClientSpec contains information about the GitHub client that this identity provider will use +for web-based login flows. + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-27-apis-supervisor-idp-v1alpha1-githubidentityproviderspec[$$GitHubIdentityProviderSpec$$] +**** + +[cols="25a,75a", options="header"] +|=== +| Field | Description +| *`secretName`* __string__ | SecretName contains the name of a namespace-local Secret object that provides the clientID and + +clientSecret for an GitHub App or GitHub OAuth2 client. + + + +This secret must be of type "secrets.pinniped.dev/github-client" with keys "clientID" and "clientSecret". + +|=== + + +[id="{anchor_prefix}-go-pinniped-dev-generated-1-27-apis-supervisor-idp-v1alpha1-githubgroupnameattribute"] +==== GitHubGroupNameAttribute (string) + +GitHubGroupNameAttribute allows the user to specify which attribute from GitHub to use for the group +names to present to Kubernetes. See the response schema for +[List teams for the authenticated user](https://docs.github.com/en/rest/teams/teams?apiVersion=2022-11-28#list-teams-for-the-authenticated-user). + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-27-apis-supervisor-idp-v1alpha1-githubclaims[$$GitHubClaims$$] +**** + + + +[id="{anchor_prefix}-go-pinniped-dev-generated-1-27-apis-supervisor-idp-v1alpha1-githubidentityprovider"] +==== GitHubIdentityProvider + +GitHubIdentityProvider describes the configuration of an upstream GitHub identity provider. +This upstream provider can be configured with either a GitHub App or a GitHub OAuth2 App. + + +Right now, only web-based logins are supported, for both the pinniped-cli client and clients configured +as OIDCClients. + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-27-apis-supervisor-idp-v1alpha1-githubidentityproviderlist[$$GitHubIdentityProviderList$$] +**** + +[cols="25a,75a", options="header"] +|=== +| Field | Description +| *`metadata`* __link:https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.27/#objectmeta-v1-meta[$$ObjectMeta$$]__ | Refer to Kubernetes API documentation for fields of `metadata`. + +| *`spec`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-27-apis-supervisor-idp-v1alpha1-githubidentityproviderspec[$$GitHubIdentityProviderSpec$$]__ | Spec for configuring the identity provider. + +| *`status`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-27-apis-supervisor-idp-v1alpha1-githubidentityproviderstatus[$$GitHubIdentityProviderStatus$$]__ | Status of the identity provider. + +|=== + + + + +[id="{anchor_prefix}-go-pinniped-dev-generated-1-27-apis-supervisor-idp-v1alpha1-githubidentityproviderphase"] +==== GitHubIdentityProviderPhase (string) + + + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-27-apis-supervisor-idp-v1alpha1-githubidentityproviderstatus[$$GitHubIdentityProviderStatus$$] +**** + + + +[id="{anchor_prefix}-go-pinniped-dev-generated-1-27-apis-supervisor-idp-v1alpha1-githubidentityproviderspec"] +==== GitHubIdentityProviderSpec + +GitHubIdentityProviderSpec is the spec for configuring an GitHub identity provider. + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-27-apis-supervisor-idp-v1alpha1-githubidentityprovider[$$GitHubIdentityProvider$$] +**** + +[cols="25a,75a", options="header"] +|=== +| Field | Description +| *`githubAPI`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-27-apis-supervisor-idp-v1alpha1-githubapiconfig[$$GitHubAPIConfig$$]__ | GitHubAPI allows configuration for GitHub Enterprise Server + +| *`claims`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-27-apis-supervisor-idp-v1alpha1-githubclaims[$$GitHubClaims$$]__ | Claims allows customization of the username and groups claims. + +| *`allowAuthentication`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-27-apis-supervisor-idp-v1alpha1-githuballowauthenticationspec[$$GitHubAllowAuthenticationSpec$$]__ | AllowAuthentication allows customization of who can authenticate using this IDP and how. + +| *`client`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-27-apis-supervisor-idp-v1alpha1-githubclientspec[$$GitHubClientSpec$$]__ | Client identifies the secret with credentials for a GitHub App or GitHub OAuth2 App (a GitHub client). + +|=== + + +[id="{anchor_prefix}-go-pinniped-dev-generated-1-27-apis-supervisor-idp-v1alpha1-githubidentityproviderstatus"] +==== GitHubIdentityProviderStatus + +GitHubIdentityProviderStatus is the status of an GitHub identity provider. + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-27-apis-supervisor-idp-v1alpha1-githubidentityprovider[$$GitHubIdentityProvider$$] +**** + +[cols="25a,75a", options="header"] +|=== +| Field | Description +| *`phase`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-27-apis-supervisor-idp-v1alpha1-githubidentityproviderphase[$$GitHubIdentityProviderPhase$$]__ | Phase summarizes the overall status of the GitHubIdentityProvider. + +| *`conditions`* __link:https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.27/#condition-v1-meta[$$Condition$$] array__ | Conditions represents the observations of an identity provider's current state. + +|=== + + +[id="{anchor_prefix}-go-pinniped-dev-generated-1-27-apis-supervisor-idp-v1alpha1-githuborganizationsspec"] +==== GitHubOrganizationsSpec + + + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-27-apis-supervisor-idp-v1alpha1-githuballowauthenticationspec[$$GitHubAllowAuthenticationSpec$$] +**** + +[cols="25a,75a", options="header"] +|=== +| Field | Description +| *`policy`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-27-apis-supervisor-idp-v1alpha1-githuballowedauthorganizationspolicy[$$GitHubAllowedAuthOrganizationsPolicy$$]__ | Policy must be set to "AllGitHubUsers" if allowed is empty. + + + +This field only exists to ensure that Pinniped administrators are aware that an empty list of + +allowedOrganizations means all GitHub users are allowed to log in. + +| *`allowed`* __string array__ | Allowed, when specified, indicates that only users with membership in at least one of the listed + +GitHub organizations may log in. In addition, the group membership presented to Kubernetes will only include + +teams within the listed GitHub organizations. Additional login rules or group filtering can optionally be + +provided as policy expression on any Pinniped Supervisor FederationDomain that includes this IDP. + + + +The configured GitHub App or GitHub OAuth App must be allowed to see membership in the listed organizations, + +otherwise Pinniped will not be aware that the user belongs to the listed organization or any teams + +within that organization. + + + +If no organizations are listed, you must set organizations: AllGitHubUsers. + +|=== + + +[id="{anchor_prefix}-go-pinniped-dev-generated-1-27-apis-supervisor-idp-v1alpha1-githubusernameattribute"] +==== GitHubUsernameAttribute (string) + +GitHubUsernameAttribute allows the user to specify which attribute(s) from GitHub to use for the username to present +to Kubernetes. See the response schema for +[Get the authenticated user](https://docs.github.com/en/rest/users/users?apiVersion=2022-11-28#get-the-authenticated-user). + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-27-apis-supervisor-idp-v1alpha1-githubclaims[$$GitHubClaims$$] +**** + + + [id="{anchor_prefix}-go-pinniped-dev-generated-1-27-apis-supervisor-idp-v1alpha1-ldapidentityprovider"] ==== LDAPIdentityProvider @@ -2108,11 +2387,12 @@ Parameter is a key/value pair which represents a parameter in an HTTP request. [id="{anchor_prefix}-go-pinniped-dev-generated-1-27-apis-supervisor-idp-v1alpha1-tlsspec"] ==== TLSSpec -Configuration for TLS parameters related to identity provider integration. +TLSSpec provides TLS configuration for identity provider integration. .Appears In: **** - xref:{anchor_prefix}-go-pinniped-dev-generated-1-27-apis-supervisor-idp-v1alpha1-activedirectoryidentityproviderspec[$$ActiveDirectoryIdentityProviderSpec$$] +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-27-apis-supervisor-idp-v1alpha1-githubapiconfig[$$GitHubAPIConfig$$] - xref:{anchor_prefix}-go-pinniped-dev-generated-1-27-apis-supervisor-idp-v1alpha1-ldapidentityproviderspec[$$LDAPIdentityProviderSpec$$] - xref:{anchor_prefix}-go-pinniped-dev-generated-1-27-apis-supervisor-idp-v1alpha1-oidcidentityproviderspec[$$OIDCIdentityProviderSpec$$] **** diff --git a/generated/1.27/apis/go.mod b/generated/1.27/apis/go.mod index 0bb4f37e3..63b7533cf 100644 --- a/generated/1.27/apis/go.mod +++ b/generated/1.27/apis/go.mod @@ -4,6 +4,6 @@ module go.pinniped.dev/generated/1.27/apis go 1.13 require ( - k8s.io/api v0.27.13 - k8s.io/apimachinery v0.27.13 + k8s.io/api v0.27.14 + k8s.io/apimachinery v0.27.14 ) diff --git a/generated/1.27/apis/go.sum b/generated/1.27/apis/go.sum index be2030f55..31da3f943 100644 --- a/generated/1.27/apis/go.sum +++ b/generated/1.27/apis/go.sum @@ -330,10 +330,10 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -k8s.io/api v0.27.13 h1:d49LYs1dh+JMMDNYQSu8FhEzCjc2TNpYvDWoSGAKs80= -k8s.io/api v0.27.13/go.mod h1:W3lYMPs34i0XQA+cmKfejve+HwbRZjy67fL05RyJUTo= -k8s.io/apimachinery v0.27.13 h1:xDAnOWaRVNSkaKdfB0Ab11hixH90KGTbLwEHMloMjFM= -k8s.io/apimachinery v0.27.13/go.mod h1:TWo+8wOIz3CytsrlI9k/LBWXLRr9dqf5hRSCbbggMAg= +k8s.io/api v0.27.14 h1:/oKAF9HiSB47polol2Ji2TaFnC400JK57jSPUXY5MzU= +k8s.io/api v0.27.14/go.mod h1:Jekhd9Kyo2CsmJlYbqZPXNwIxiHvyGJCdp0X56yDyvU= +k8s.io/apimachinery v0.27.14 h1:jAIGvPbvAg4XJysK7JPFa6DdjTR6vts4/p4Q6ZrcQ+4= +k8s.io/apimachinery v0.27.14/go.mod h1:TWo+8wOIz3CytsrlI9k/LBWXLRr9dqf5hRSCbbggMAg= k8s.io/gengo v0.0.0-20210813121822-485abfe95c7c/go.mod h1:FiNAH4ZV3gBg2Kwh89tzAEV2be7d5xI0vBa/VySYy3E= k8s.io/klog/v2 v2.0.0/go.mod h1:PBfzABfn139FHAV07az/IF9Wp1bkk3vpT2XSJ76fSDE= k8s.io/klog/v2 v2.2.0/go.mod h1:Od+F08eJP+W3HUb4pSrPpgp9DGU4GzlpG/TmITuYh/Y= diff --git a/generated/1.27/apis/supervisor/idp/v1alpha1/register.go b/generated/1.27/apis/supervisor/idp/v1alpha1/register.go index 8829a8638..705be8076 100644 --- a/generated/1.27/apis/supervisor/idp/v1alpha1/register.go +++ b/generated/1.27/apis/supervisor/idp/v1alpha1/register.go @@ -1,4 +1,4 @@ -// Copyright 2020-2022 the Pinniped contributors. All Rights Reserved. +// Copyright 2020-2024 the Pinniped contributors. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 package v1alpha1 @@ -36,6 +36,8 @@ func addKnownTypes(scheme *runtime.Scheme) error { &LDAPIdentityProviderList{}, &ActiveDirectoryIdentityProvider{}, &ActiveDirectoryIdentityProviderList{}, + &GitHubIdentityProvider{}, + &GitHubIdentityProviderList{}, ) metav1.AddToGroupVersion(scheme, SchemeGroupVersion) return nil diff --git a/generated/1.27/apis/supervisor/idp/v1alpha1/types_githubidentityprovider.go b/generated/1.27/apis/supervisor/idp/v1alpha1/types_githubidentityprovider.go new file mode 100644 index 000000000..c84f46dbd --- /dev/null +++ b/generated/1.27/apis/supervisor/idp/v1alpha1/types_githubidentityprovider.go @@ -0,0 +1,256 @@ +// Copyright 2024 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package v1alpha1 + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +type GitHubIdentityProviderPhase string + +const ( + // GitHubPhasePending is the default phase for newly-created GitHubIdentityProvider resources. + GitHubPhasePending GitHubIdentityProviderPhase = "Pending" + + // GitHubPhaseReady is the phase for an GitHubIdentityProvider resource in a healthy state. + GitHubPhaseReady GitHubIdentityProviderPhase = "Ready" + + // GitHubPhaseError is the phase for an GitHubIdentityProvider in an unhealthy state. + GitHubPhaseError GitHubIdentityProviderPhase = "Error" +) + +type GitHubAllowedAuthOrganizationsPolicy string + +const ( + // GitHubAllowedAuthOrganizationsPolicyAllGitHubUsers means any GitHub user is allowed to log in using this identity + // provider, regardless of their organization membership or lack thereof. + GitHubAllowedAuthOrganizationsPolicyAllGitHubUsers GitHubAllowedAuthOrganizationsPolicy = "AllGitHubUsers" + + // GitHubAllowedAuthOrganizationsPolicyOnlyUsersFromAllowedOrganizations means only those users with membership in + // the listed GitHub organizations are allowed to log in. + GitHubAllowedAuthOrganizationsPolicyOnlyUsersFromAllowedOrganizations GitHubAllowedAuthOrganizationsPolicy = "OnlyUsersFromAllowedOrganizations" +) + +// GitHubIdentityProviderStatus is the status of an GitHub identity provider. +type GitHubIdentityProviderStatus struct { + // Phase summarizes the overall status of the GitHubIdentityProvider. + // + // +kubebuilder:default=Pending + // +kubebuilder:validation:Enum=Pending;Ready;Error + Phase GitHubIdentityProviderPhase `json:"phase,omitempty"` + + // Conditions represents the observations of an identity provider's current state. + // + // +patchMergeKey=type + // +patchStrategy=merge + // +listType=map + // +listMapKey=type + Conditions []metav1.Condition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type"` +} + +// GitHubAPIConfig allows configuration for GitHub Enterprise Server +type GitHubAPIConfig struct { + // Host is required only for GitHub Enterprise Server. + // Defaults to using GitHub's public API ("github.com"). + // Do not specify a protocol or scheme since "https://" will always be used. + // Port is optional. Do not specify a path, query, fragment, or userinfo. + // Only domain name or IP address, subdomains (optional), and port (optional). + // IPv4 and IPv6 are supported. If using an IPv6 address with a port, you must enclose the IPv6 address + // in square brackets. Example: "[::1]:443". + // + // +kubebuilder:default="github.com" + // +kubebuilder:validation:MinLength=1 + // +optional + Host *string `json:"host"` + + // TLS configuration for GitHub Enterprise Server. + // + // +optional + TLS *TLSSpec `json:"tls,omitempty"` +} + +// GitHubUsernameAttribute allows the user to specify which attribute(s) from GitHub to use for the username to present +// to Kubernetes. See the response schema for +// [Get the authenticated user](https://docs.github.com/en/rest/users/users?apiVersion=2022-11-28#get-the-authenticated-user). +type GitHubUsernameAttribute string + +const ( + // GitHubUsernameID specifies using the `id` attribute from the GitHub user for the username to present to Kubernetes. + GitHubUsernameID GitHubUsernameAttribute = "id" + + // GitHubUsernameLogin specifies using the `login` attribute from the GitHub user as the username to present to Kubernetes. + GitHubUsernameLogin GitHubUsernameAttribute = "login" + + // GitHubUsernameLoginAndID specifies combining the `login` and `id` attributes from the GitHub user as the + // username to present to Kubernetes, separated by a colon. Example: "my-login:1234" + GitHubUsernameLoginAndID GitHubUsernameAttribute = "login:id" +) + +// GitHubGroupNameAttribute allows the user to specify which attribute from GitHub to use for the group +// names to present to Kubernetes. See the response schema for +// [List teams for the authenticated user](https://docs.github.com/en/rest/teams/teams?apiVersion=2022-11-28#list-teams-for-the-authenticated-user). +type GitHubGroupNameAttribute string + +const ( + // GitHubUseTeamNameForGroupName specifies using the GitHub team's `name` attribute as the group name to present to Kubernetes. + GitHubUseTeamNameForGroupName GitHubGroupNameAttribute = "name" + + // GitHubUseTeamSlugForGroupName specifies using the GitHub team's `slug` attribute as the group name to present to Kubernetes. + GitHubUseTeamSlugForGroupName GitHubGroupNameAttribute = "slug" +) + +// GitHubClaims allows customization of the username and groups claims. +type GitHubClaims struct { + // Username configures which property of the GitHub user record shall determine the username in Kubernetes. + // + // Can be either "id", "login", or "login:id". Defaults to "login:id". + // + // GitHub's user login attributes can only contain alphanumeric characters and non-repeating hyphens, + // and may not start or end with hyphens. GitHub users are allowed to change their login name, + // although it is inconvenient. If a GitHub user changed their login name from "foo" to "bar", + // then a second user might change their name from "baz" to "foo" in order to take the old + // username of the first user. For this reason, it is not as safe to make authorization decisions + // based only on the user's login attribute. + // + // If desired, an admin could configure identity transformation expressions on the Pinniped Supervisor's + // FederationDomain to further customize how these usernames are presented to Kubernetes. + // + // Defaults to "login:id", which is the user login attribute, followed by a colon, followed by the unique and + // unchanging integer ID number attribute. This blends human-readable login names with the unchanging ID value + // from GitHub. Colons are not allowed in GitHub login attributes or ID numbers, so this is a reasonable + // choice to concatenate the two values. + // + // See the response schema for + // [Get the authenticated user](https://docs.github.com/en/rest/users/users?apiVersion=2022-11-28#get-the-authenticated-user). + // + // +kubebuilder:default="login:id" + // +kubebuilder:validation:Enum={"id","login","login:id"} + // +optional + Username *GitHubUsernameAttribute `json:"username"` + + // Groups configures which property of the GitHub team record shall determine the group names in Kubernetes. + // + // Can be either "name" or "slug". Defaults to "slug". + // + // GitHub team names can contain upper and lower case characters, whitespace, and punctuation (e.g. "Kube admins!"). + // + // GitHub team slugs are lower case alphanumeric characters and may contain dashes and underscores (e.g. "kube-admins"). + // + // Group names as presented to Kubernetes will always be prefixed by the GitHub organization name followed by a + // forward slash (e.g. "my-org/my-team"). GitHub organization login names can only contain alphanumeric characters + // or single hyphens, so the first forward slash `/` will be the separator between the organization login name and + // the team name or slug. + // + // If desired, an admin could configure identity transformation expressions on the Pinniped Supervisor's + // FederationDomain to further customize how these group names are presented to Kubernetes. + // + // See the response schema for + // [List teams for the authenticated user](https://docs.github.com/en/rest/teams/teams?apiVersion=2022-11-28#list-teams-for-the-authenticated-user). + // + // +kubebuilder:default=slug + // +kubebuilder:validation:Enum=name;slug + // +optional + Groups *GitHubGroupNameAttribute `json:"groups"` +} + +// GitHubClientSpec contains information about the GitHub client that this identity provider will use +// for web-based login flows. +type GitHubClientSpec struct { + // SecretName contains the name of a namespace-local Secret object that provides the clientID and + // clientSecret for an GitHub App or GitHub OAuth2 client. + // + // This secret must be of type "secrets.pinniped.dev/github-client" with keys "clientID" and "clientSecret". + // + // +kubebuilder:validation:MinLength=1 + SecretName string `json:"secretName"` +} + +type GitHubOrganizationsSpec struct { + // Policy must be set to "AllGitHubUsers" if allowed is empty. + // + // This field only exists to ensure that Pinniped administrators are aware that an empty list of + // allowedOrganizations means all GitHub users are allowed to log in. + // + // +kubebuilder:default=OnlyUsersFromAllowedOrganizations + // +kubebuilder:validation:Enum=OnlyUsersFromAllowedOrganizations;AllGitHubUsers + // +optional + Policy *GitHubAllowedAuthOrganizationsPolicy `json:"policy"` + + // Allowed, when specified, indicates that only users with membership in at least one of the listed + // GitHub organizations may log in. In addition, the group membership presented to Kubernetes will only include + // teams within the listed GitHub organizations. Additional login rules or group filtering can optionally be + // provided as policy expression on any Pinniped Supervisor FederationDomain that includes this IDP. + // + // The configured GitHub App or GitHub OAuth App must be allowed to see membership in the listed organizations, + // otherwise Pinniped will not be aware that the user belongs to the listed organization or any teams + // within that organization. + // + // If no organizations are listed, you must set organizations: AllGitHubUsers. + // + // +kubebuilder:validation:MaxItems=64 + // +listType=set + // +optional + Allowed []string `json:"allowed,omitempty"` +} + +// GitHubAllowAuthenticationSpec allows customization of who can authenticate using this IDP and how. +type GitHubAllowAuthenticationSpec struct { + // Organizations allows customization of which organizations can authenticate using this IDP. + // +kubebuilder:validation:XValidation:message="spec.allowAuthentication.organizations.policy must be 'OnlyUsersFromAllowedOrganizations' when spec.allowAuthentication.organizations.allowed has organizations listed",rule="!(has(self.allowed) && size(self.allowed) > 0 && self.policy == 'AllGitHubUsers')" + // +kubebuilder:validation:XValidation:message="spec.allowAuthentication.organizations.policy must be 'AllGitHubUsers' when spec.allowAuthentication.organizations.allowed is empty",rule="!((!has(self.allowed) || size(self.allowed) == 0) && self.policy == 'OnlyUsersFromAllowedOrganizations')" + Organizations GitHubOrganizationsSpec `json:"organizations"` +} + +// GitHubIdentityProviderSpec is the spec for configuring an GitHub identity provider. +type GitHubIdentityProviderSpec struct { + // GitHubAPI allows configuration for GitHub Enterprise Server + // + // +kubebuilder:default={} + GitHubAPI GitHubAPIConfig `json:"githubAPI,omitempty"` + + // Claims allows customization of the username and groups claims. + // + // +kubebuilder:default={} + Claims GitHubClaims `json:"claims,omitempty"` + + // AllowAuthentication allows customization of who can authenticate using this IDP and how. + AllowAuthentication GitHubAllowAuthenticationSpec `json:"allowAuthentication"` + + // Client identifies the secret with credentials for a GitHub App or GitHub OAuth2 App (a GitHub client). + Client GitHubClientSpec `json:"client"` +} + +// GitHubIdentityProvider describes the configuration of an upstream GitHub identity provider. +// This upstream provider can be configured with either a GitHub App or a GitHub OAuth2 App. +// +// Right now, only web-based logins are supported, for both the pinniped-cli client and clients configured +// as OIDCClients. +// +// +genclient +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object +// +kubebuilder:resource:categories=pinniped;pinniped-idp;pinniped-idps +// +kubebuilder:printcolumn:name="Host",type=string,JSONPath=`.spec.githubAPI.host` +// +kubebuilder:printcolumn:name="Status",type=string,JSONPath=`.status.phase` +// +kubebuilder:printcolumn:name="Age",type=date,JSONPath=`.metadata.creationTimestamp` +// +kubebuilder:subresource:status +type GitHubIdentityProvider struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + // Spec for configuring the identity provider. + Spec GitHubIdentityProviderSpec `json:"spec"` + + // Status of the identity provider. + Status GitHubIdentityProviderStatus `json:"status,omitempty"` +} + +// GitHubIdentityProviderList lists GitHubIdentityProvider objects. +// +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object +type GitHubIdentityProviderList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + + Items []GitHubIdentityProvider `json:"items"` +} diff --git a/generated/1.27/apis/supervisor/idp/v1alpha1/types_tls.go b/generated/1.27/apis/supervisor/idp/v1alpha1/types_tls.go index 1413a262c..49b49373c 100644 --- a/generated/1.27/apis/supervisor/idp/v1alpha1/types_tls.go +++ b/generated/1.27/apis/supervisor/idp/v1alpha1/types_tls.go @@ -1,9 +1,9 @@ -// Copyright 2020-2022 the Pinniped contributors. All Rights Reserved. +// Copyright 2020-2024 the Pinniped contributors. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 package v1alpha1 -// Configuration for TLS parameters related to identity provider integration. +// TLSSpec provides TLS configuration for identity provider integration. type TLSSpec struct { // X.509 Certificate Authority (base64-encoded PEM bundle). If omitted, a default set of system roots will be trusted. // +optional diff --git a/generated/1.27/apis/supervisor/idp/v1alpha1/zz_generated.deepcopy.go b/generated/1.27/apis/supervisor/idp/v1alpha1/zz_generated.deepcopy.go index b0cb168b5..e48860e82 100644 --- a/generated/1.27/apis/supervisor/idp/v1alpha1/zz_generated.deepcopy.go +++ b/generated/1.27/apis/supervisor/idp/v1alpha1/zz_generated.deepcopy.go @@ -203,6 +203,221 @@ func (in *ActiveDirectoryIdentityProviderUserSearchAttributes) DeepCopy() *Activ return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *GitHubAPIConfig) DeepCopyInto(out *GitHubAPIConfig) { + *out = *in + if in.Host != nil { + in, out := &in.Host, &out.Host + *out = new(string) + **out = **in + } + if in.TLS != nil { + in, out := &in.TLS, &out.TLS + *out = new(TLSSpec) + **out = **in + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GitHubAPIConfig. +func (in *GitHubAPIConfig) DeepCopy() *GitHubAPIConfig { + if in == nil { + return nil + } + out := new(GitHubAPIConfig) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *GitHubAllowAuthenticationSpec) DeepCopyInto(out *GitHubAllowAuthenticationSpec) { + *out = *in + in.Organizations.DeepCopyInto(&out.Organizations) + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GitHubAllowAuthenticationSpec. +func (in *GitHubAllowAuthenticationSpec) DeepCopy() *GitHubAllowAuthenticationSpec { + if in == nil { + return nil + } + out := new(GitHubAllowAuthenticationSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *GitHubClaims) DeepCopyInto(out *GitHubClaims) { + *out = *in + if in.Username != nil { + in, out := &in.Username, &out.Username + *out = new(GitHubUsernameAttribute) + **out = **in + } + if in.Groups != nil { + in, out := &in.Groups, &out.Groups + *out = new(GitHubGroupNameAttribute) + **out = **in + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GitHubClaims. +func (in *GitHubClaims) DeepCopy() *GitHubClaims { + if in == nil { + return nil + } + out := new(GitHubClaims) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *GitHubClientSpec) DeepCopyInto(out *GitHubClientSpec) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GitHubClientSpec. +func (in *GitHubClientSpec) DeepCopy() *GitHubClientSpec { + if in == nil { + return nil + } + out := new(GitHubClientSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *GitHubIdentityProvider) DeepCopyInto(out *GitHubIdentityProvider) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + in.Status.DeepCopyInto(&out.Status) + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GitHubIdentityProvider. +func (in *GitHubIdentityProvider) DeepCopy() *GitHubIdentityProvider { + if in == nil { + return nil + } + out := new(GitHubIdentityProvider) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *GitHubIdentityProvider) 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 *GitHubIdentityProviderList) DeepCopyInto(out *GitHubIdentityProviderList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]GitHubIdentityProvider, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GitHubIdentityProviderList. +func (in *GitHubIdentityProviderList) DeepCopy() *GitHubIdentityProviderList { + if in == nil { + return nil + } + out := new(GitHubIdentityProviderList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *GitHubIdentityProviderList) 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 *GitHubIdentityProviderSpec) DeepCopyInto(out *GitHubIdentityProviderSpec) { + *out = *in + in.GitHubAPI.DeepCopyInto(&out.GitHubAPI) + in.Claims.DeepCopyInto(&out.Claims) + in.AllowAuthentication.DeepCopyInto(&out.AllowAuthentication) + out.Client = in.Client + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GitHubIdentityProviderSpec. +func (in *GitHubIdentityProviderSpec) DeepCopy() *GitHubIdentityProviderSpec { + if in == nil { + return nil + } + out := new(GitHubIdentityProviderSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *GitHubIdentityProviderStatus) DeepCopyInto(out *GitHubIdentityProviderStatus) { + *out = *in + if in.Conditions != nil { + in, out := &in.Conditions, &out.Conditions + *out = make([]v1.Condition, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GitHubIdentityProviderStatus. +func (in *GitHubIdentityProviderStatus) DeepCopy() *GitHubIdentityProviderStatus { + if in == nil { + return nil + } + out := new(GitHubIdentityProviderStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *GitHubOrganizationsSpec) DeepCopyInto(out *GitHubOrganizationsSpec) { + *out = *in + if in.Policy != nil { + in, out := &in.Policy, &out.Policy + *out = new(GitHubAllowedAuthOrganizationsPolicy) + **out = **in + } + if in.Allowed != nil { + in, out := &in.Allowed, &out.Allowed + *out = make([]string, len(*in)) + copy(*out, *in) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GitHubOrganizationsSpec. +func (in *GitHubOrganizationsSpec) DeepCopy() *GitHubOrganizationsSpec { + if in == nil { + return nil + } + out := new(GitHubOrganizationsSpec) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *LDAPIdentityProvider) DeepCopyInto(out *LDAPIdentityProvider) { *out = *in diff --git a/generated/1.27/apis/supervisor/idpdiscovery/v1alpha1/types_supervisor_idp_discovery.go b/generated/1.27/apis/supervisor/idpdiscovery/v1alpha1/types_supervisor_idp_discovery.go index 837fed2dd..11be6f9dc 100644 --- a/generated/1.27/apis/supervisor/idpdiscovery/v1alpha1/types_supervisor_idp_discovery.go +++ b/generated/1.27/apis/supervisor/idpdiscovery/v1alpha1/types_supervisor_idp_discovery.go @@ -15,6 +15,7 @@ const ( IDPTypeOIDC IDPType = "oidc" IDPTypeLDAP IDPType = "ldap" IDPTypeActiveDirectory IDPType = "activedirectory" + IDPTypeGitHub IDPType = "github" IDPFlowCLIPassword IDPFlow = "cli_password" IDPFlowBrowserAuthcode IDPFlow = "browser_authcode" diff --git a/generated/1.27/client/go.mod b/generated/1.27/client/go.mod index 2f809619a..ded26c0e1 100644 --- a/generated/1.27/client/go.mod +++ b/generated/1.27/client/go.mod @@ -7,7 +7,7 @@ replace go.pinniped.dev/generated/1.27/apis => ../apis require ( go.pinniped.dev/generated/1.27/apis v0.0.0 - k8s.io/apimachinery v0.27.13 - k8s.io/client-go v0.27.13 + k8s.io/apimachinery v0.27.14 + k8s.io/client-go v0.27.14 k8s.io/kube-openapi v0.0.0-20230501164219-8b0f38b5fd1f ) diff --git a/generated/1.27/client/go.sum b/generated/1.27/client/go.sum index dbfca3259..a06073cf4 100644 --- a/generated/1.27/client/go.sum +++ b/generated/1.27/client/go.sum @@ -370,12 +370,12 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -k8s.io/api v0.27.13 h1:d49LYs1dh+JMMDNYQSu8FhEzCjc2TNpYvDWoSGAKs80= -k8s.io/api v0.27.13/go.mod h1:W3lYMPs34i0XQA+cmKfejve+HwbRZjy67fL05RyJUTo= -k8s.io/apimachinery v0.27.13 h1:xDAnOWaRVNSkaKdfB0Ab11hixH90KGTbLwEHMloMjFM= -k8s.io/apimachinery v0.27.13/go.mod h1:TWo+8wOIz3CytsrlI9k/LBWXLRr9dqf5hRSCbbggMAg= -k8s.io/client-go v0.27.13 h1:SfUbIukb6BSqaadlYRX0AzMoN6+e+9FZGEKqfisidho= -k8s.io/client-go v0.27.13/go.mod h1:I9SBaI28r6ii465Fb0dTpf5O3adOnDwNBoeqlDNbbFg= +k8s.io/api v0.27.14 h1:/oKAF9HiSB47polol2Ji2TaFnC400JK57jSPUXY5MzU= +k8s.io/api v0.27.14/go.mod h1:Jekhd9Kyo2CsmJlYbqZPXNwIxiHvyGJCdp0X56yDyvU= +k8s.io/apimachinery v0.27.14 h1:jAIGvPbvAg4XJysK7JPFa6DdjTR6vts4/p4Q6ZrcQ+4= +k8s.io/apimachinery v0.27.14/go.mod h1:TWo+8wOIz3CytsrlI9k/LBWXLRr9dqf5hRSCbbggMAg= +k8s.io/client-go v0.27.14 h1:5KwfSakOTQFRlPru2Ql/wp1URjPgzoP7QpTlEH9a+ys= +k8s.io/client-go v0.27.14/go.mod h1:cy+p3ijvbPQpdcwg01qnHBmkYDtbOatNC83anA9y18g= k8s.io/gengo v0.0.0-20210813121822-485abfe95c7c/go.mod h1:FiNAH4ZV3gBg2Kwh89tzAEV2be7d5xI0vBa/VySYy3E= k8s.io/klog/v2 v2.0.0/go.mod h1:PBfzABfn139FHAV07az/IF9Wp1bkk3vpT2XSJ76fSDE= k8s.io/klog/v2 v2.2.0/go.mod h1:Od+F08eJP+W3HUb4pSrPpgp9DGU4GzlpG/TmITuYh/Y= diff --git a/generated/1.27/client/supervisor/clientset/versioned/typed/idp/v1alpha1/fake/fake_githubidentityprovider.go b/generated/1.27/client/supervisor/clientset/versioned/typed/idp/v1alpha1/fake/fake_githubidentityprovider.go new file mode 100644 index 000000000..0f5fc1d90 --- /dev/null +++ b/generated/1.27/client/supervisor/clientset/versioned/typed/idp/v1alpha1/fake/fake_githubidentityprovider.go @@ -0,0 +1,128 @@ +// Copyright 2020-2024 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +// Code generated by client-gen. DO NOT EDIT. + +package fake + +import ( + "context" + + v1alpha1 "go.pinniped.dev/generated/1.27/apis/supervisor/idp/v1alpha1" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + labels "k8s.io/apimachinery/pkg/labels" + types "k8s.io/apimachinery/pkg/types" + watch "k8s.io/apimachinery/pkg/watch" + testing "k8s.io/client-go/testing" +) + +// FakeGitHubIdentityProviders implements GitHubIdentityProviderInterface +type FakeGitHubIdentityProviders struct { + Fake *FakeIDPV1alpha1 + ns string +} + +var githubidentityprovidersResource = v1alpha1.SchemeGroupVersion.WithResource("githubidentityproviders") + +var githubidentityprovidersKind = v1alpha1.SchemeGroupVersion.WithKind("GitHubIdentityProvider") + +// Get takes name of the gitHubIdentityProvider, and returns the corresponding gitHubIdentityProvider object, and an error if there is any. +func (c *FakeGitHubIdentityProviders) Get(ctx context.Context, name string, options v1.GetOptions) (result *v1alpha1.GitHubIdentityProvider, err error) { + obj, err := c.Fake. + Invokes(testing.NewGetAction(githubidentityprovidersResource, c.ns, name), &v1alpha1.GitHubIdentityProvider{}) + + if obj == nil { + return nil, err + } + return obj.(*v1alpha1.GitHubIdentityProvider), err +} + +// List takes label and field selectors, and returns the list of GitHubIdentityProviders that match those selectors. +func (c *FakeGitHubIdentityProviders) List(ctx context.Context, opts v1.ListOptions) (result *v1alpha1.GitHubIdentityProviderList, err error) { + obj, err := c.Fake. + Invokes(testing.NewListAction(githubidentityprovidersResource, githubidentityprovidersKind, c.ns, opts), &v1alpha1.GitHubIdentityProviderList{}) + + if obj == nil { + return nil, err + } + + label, _, _ := testing.ExtractFromListOptions(opts) + if label == nil { + label = labels.Everything() + } + list := &v1alpha1.GitHubIdentityProviderList{ListMeta: obj.(*v1alpha1.GitHubIdentityProviderList).ListMeta} + for _, item := range obj.(*v1alpha1.GitHubIdentityProviderList).Items { + if label.Matches(labels.Set(item.Labels)) { + list.Items = append(list.Items, item) + } + } + return list, err +} + +// Watch returns a watch.Interface that watches the requested gitHubIdentityProviders. +func (c *FakeGitHubIdentityProviders) Watch(ctx context.Context, opts v1.ListOptions) (watch.Interface, error) { + return c.Fake. + InvokesWatch(testing.NewWatchAction(githubidentityprovidersResource, c.ns, opts)) + +} + +// Create takes the representation of a gitHubIdentityProvider and creates it. Returns the server's representation of the gitHubIdentityProvider, and an error, if there is any. +func (c *FakeGitHubIdentityProviders) Create(ctx context.Context, gitHubIdentityProvider *v1alpha1.GitHubIdentityProvider, opts v1.CreateOptions) (result *v1alpha1.GitHubIdentityProvider, err error) { + obj, err := c.Fake. + Invokes(testing.NewCreateAction(githubidentityprovidersResource, c.ns, gitHubIdentityProvider), &v1alpha1.GitHubIdentityProvider{}) + + if obj == nil { + return nil, err + } + return obj.(*v1alpha1.GitHubIdentityProvider), err +} + +// Update takes the representation of a gitHubIdentityProvider and updates it. Returns the server's representation of the gitHubIdentityProvider, and an error, if there is any. +func (c *FakeGitHubIdentityProviders) Update(ctx context.Context, gitHubIdentityProvider *v1alpha1.GitHubIdentityProvider, opts v1.UpdateOptions) (result *v1alpha1.GitHubIdentityProvider, err error) { + obj, err := c.Fake. + Invokes(testing.NewUpdateAction(githubidentityprovidersResource, c.ns, gitHubIdentityProvider), &v1alpha1.GitHubIdentityProvider{}) + + if obj == nil { + return nil, err + } + return obj.(*v1alpha1.GitHubIdentityProvider), err +} + +// UpdateStatus was generated because the type contains a Status member. +// Add a +genclient:noStatus comment above the type to avoid generating UpdateStatus(). +func (c *FakeGitHubIdentityProviders) UpdateStatus(ctx context.Context, gitHubIdentityProvider *v1alpha1.GitHubIdentityProvider, opts v1.UpdateOptions) (*v1alpha1.GitHubIdentityProvider, error) { + obj, err := c.Fake. + Invokes(testing.NewUpdateSubresourceAction(githubidentityprovidersResource, "status", c.ns, gitHubIdentityProvider), &v1alpha1.GitHubIdentityProvider{}) + + if obj == nil { + return nil, err + } + return obj.(*v1alpha1.GitHubIdentityProvider), err +} + +// Delete takes name of the gitHubIdentityProvider and deletes it. Returns an error if one occurs. +func (c *FakeGitHubIdentityProviders) Delete(ctx context.Context, name string, opts v1.DeleteOptions) error { + _, err := c.Fake. + Invokes(testing.NewDeleteActionWithOptions(githubidentityprovidersResource, c.ns, name, opts), &v1alpha1.GitHubIdentityProvider{}) + + return err +} + +// DeleteCollection deletes a collection of objects. +func (c *FakeGitHubIdentityProviders) DeleteCollection(ctx context.Context, opts v1.DeleteOptions, listOpts v1.ListOptions) error { + action := testing.NewDeleteCollectionAction(githubidentityprovidersResource, c.ns, listOpts) + + _, err := c.Fake.Invokes(action, &v1alpha1.GitHubIdentityProviderList{}) + return err +} + +// Patch applies the patch and returns the patched gitHubIdentityProvider. +func (c *FakeGitHubIdentityProviders) Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts v1.PatchOptions, subresources ...string) (result *v1alpha1.GitHubIdentityProvider, err error) { + obj, err := c.Fake. + Invokes(testing.NewPatchSubresourceAction(githubidentityprovidersResource, c.ns, name, pt, data, subresources...), &v1alpha1.GitHubIdentityProvider{}) + + if obj == nil { + return nil, err + } + return obj.(*v1alpha1.GitHubIdentityProvider), err +} diff --git a/generated/1.27/client/supervisor/clientset/versioned/typed/idp/v1alpha1/fake/fake_idp_client.go b/generated/1.27/client/supervisor/clientset/versioned/typed/idp/v1alpha1/fake/fake_idp_client.go index a40338a69..f29ec9e31 100644 --- a/generated/1.27/client/supervisor/clientset/versioned/typed/idp/v1alpha1/fake/fake_idp_client.go +++ b/generated/1.27/client/supervisor/clientset/versioned/typed/idp/v1alpha1/fake/fake_idp_client.go @@ -19,6 +19,10 @@ func (c *FakeIDPV1alpha1) ActiveDirectoryIdentityProviders(namespace string) v1a return &FakeActiveDirectoryIdentityProviders{c, namespace} } +func (c *FakeIDPV1alpha1) GitHubIdentityProviders(namespace string) v1alpha1.GitHubIdentityProviderInterface { + return &FakeGitHubIdentityProviders{c, namespace} +} + func (c *FakeIDPV1alpha1) LDAPIdentityProviders(namespace string) v1alpha1.LDAPIdentityProviderInterface { return &FakeLDAPIdentityProviders{c, namespace} } diff --git a/generated/1.27/client/supervisor/clientset/versioned/typed/idp/v1alpha1/generated_expansion.go b/generated/1.27/client/supervisor/clientset/versioned/typed/idp/v1alpha1/generated_expansion.go index 79be098d6..a7acc8120 100644 --- a/generated/1.27/client/supervisor/clientset/versioned/typed/idp/v1alpha1/generated_expansion.go +++ b/generated/1.27/client/supervisor/clientset/versioned/typed/idp/v1alpha1/generated_expansion.go @@ -7,6 +7,8 @@ package v1alpha1 type ActiveDirectoryIdentityProviderExpansion interface{} +type GitHubIdentityProviderExpansion interface{} + type LDAPIdentityProviderExpansion interface{} type OIDCIdentityProviderExpansion interface{} diff --git a/generated/1.27/client/supervisor/clientset/versioned/typed/idp/v1alpha1/githubidentityprovider.go b/generated/1.27/client/supervisor/clientset/versioned/typed/idp/v1alpha1/githubidentityprovider.go new file mode 100644 index 000000000..e56865cd4 --- /dev/null +++ b/generated/1.27/client/supervisor/clientset/versioned/typed/idp/v1alpha1/githubidentityprovider.go @@ -0,0 +1,182 @@ +// Copyright 2020-2024 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +// Code generated by client-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + "context" + "time" + + v1alpha1 "go.pinniped.dev/generated/1.27/apis/supervisor/idp/v1alpha1" + scheme "go.pinniped.dev/generated/1.27/client/supervisor/clientset/versioned/scheme" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + types "k8s.io/apimachinery/pkg/types" + watch "k8s.io/apimachinery/pkg/watch" + rest "k8s.io/client-go/rest" +) + +// GitHubIdentityProvidersGetter has a method to return a GitHubIdentityProviderInterface. +// A group's client should implement this interface. +type GitHubIdentityProvidersGetter interface { + GitHubIdentityProviders(namespace string) GitHubIdentityProviderInterface +} + +// GitHubIdentityProviderInterface has methods to work with GitHubIdentityProvider resources. +type GitHubIdentityProviderInterface interface { + Create(ctx context.Context, gitHubIdentityProvider *v1alpha1.GitHubIdentityProvider, opts v1.CreateOptions) (*v1alpha1.GitHubIdentityProvider, error) + Update(ctx context.Context, gitHubIdentityProvider *v1alpha1.GitHubIdentityProvider, opts v1.UpdateOptions) (*v1alpha1.GitHubIdentityProvider, error) + UpdateStatus(ctx context.Context, gitHubIdentityProvider *v1alpha1.GitHubIdentityProvider, opts v1.UpdateOptions) (*v1alpha1.GitHubIdentityProvider, 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) (*v1alpha1.GitHubIdentityProvider, error) + List(ctx context.Context, opts v1.ListOptions) (*v1alpha1.GitHubIdentityProviderList, 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 *v1alpha1.GitHubIdentityProvider, err error) + GitHubIdentityProviderExpansion +} + +// gitHubIdentityProviders implements GitHubIdentityProviderInterface +type gitHubIdentityProviders struct { + client rest.Interface + ns string +} + +// newGitHubIdentityProviders returns a GitHubIdentityProviders +func newGitHubIdentityProviders(c *IDPV1alpha1Client, namespace string) *gitHubIdentityProviders { + return &gitHubIdentityProviders{ + client: c.RESTClient(), + ns: namespace, + } +} + +// Get takes name of the gitHubIdentityProvider, and returns the corresponding gitHubIdentityProvider object, and an error if there is any. +func (c *gitHubIdentityProviders) Get(ctx context.Context, name string, options v1.GetOptions) (result *v1alpha1.GitHubIdentityProvider, err error) { + result = &v1alpha1.GitHubIdentityProvider{} + err = c.client.Get(). + Namespace(c.ns). + Resource("githubidentityproviders"). + Name(name). + VersionedParams(&options, scheme.ParameterCodec). + Do(ctx). + Into(result) + return +} + +// List takes label and field selectors, and returns the list of GitHubIdentityProviders that match those selectors. +func (c *gitHubIdentityProviders) List(ctx context.Context, opts v1.ListOptions) (result *v1alpha1.GitHubIdentityProviderList, err error) { + var timeout time.Duration + if opts.TimeoutSeconds != nil { + timeout = time.Duration(*opts.TimeoutSeconds) * time.Second + } + result = &v1alpha1.GitHubIdentityProviderList{} + err = c.client.Get(). + Namespace(c.ns). + Resource("githubidentityproviders"). + VersionedParams(&opts, scheme.ParameterCodec). + Timeout(timeout). + Do(ctx). + Into(result) + return +} + +// Watch returns a watch.Interface that watches the requested gitHubIdentityProviders. +func (c *gitHubIdentityProviders) Watch(ctx context.Context, opts v1.ListOptions) (watch.Interface, error) { + var timeout time.Duration + if opts.TimeoutSeconds != nil { + timeout = time.Duration(*opts.TimeoutSeconds) * time.Second + } + opts.Watch = true + return c.client.Get(). + Namespace(c.ns). + Resource("githubidentityproviders"). + VersionedParams(&opts, scheme.ParameterCodec). + Timeout(timeout). + Watch(ctx) +} + +// Create takes the representation of a gitHubIdentityProvider and creates it. Returns the server's representation of the gitHubIdentityProvider, and an error, if there is any. +func (c *gitHubIdentityProviders) Create(ctx context.Context, gitHubIdentityProvider *v1alpha1.GitHubIdentityProvider, opts v1.CreateOptions) (result *v1alpha1.GitHubIdentityProvider, err error) { + result = &v1alpha1.GitHubIdentityProvider{} + err = c.client.Post(). + Namespace(c.ns). + Resource("githubidentityproviders"). + VersionedParams(&opts, scheme.ParameterCodec). + Body(gitHubIdentityProvider). + Do(ctx). + Into(result) + return +} + +// Update takes the representation of a gitHubIdentityProvider and updates it. Returns the server's representation of the gitHubIdentityProvider, and an error, if there is any. +func (c *gitHubIdentityProviders) Update(ctx context.Context, gitHubIdentityProvider *v1alpha1.GitHubIdentityProvider, opts v1.UpdateOptions) (result *v1alpha1.GitHubIdentityProvider, err error) { + result = &v1alpha1.GitHubIdentityProvider{} + err = c.client.Put(). + Namespace(c.ns). + Resource("githubidentityproviders"). + Name(gitHubIdentityProvider.Name). + VersionedParams(&opts, scheme.ParameterCodec). + Body(gitHubIdentityProvider). + Do(ctx). + Into(result) + return +} + +// UpdateStatus was generated because the type contains a Status member. +// Add a +genclient:noStatus comment above the type to avoid generating UpdateStatus(). +func (c *gitHubIdentityProviders) UpdateStatus(ctx context.Context, gitHubIdentityProvider *v1alpha1.GitHubIdentityProvider, opts v1.UpdateOptions) (result *v1alpha1.GitHubIdentityProvider, err error) { + result = &v1alpha1.GitHubIdentityProvider{} + err = c.client.Put(). + Namespace(c.ns). + Resource("githubidentityproviders"). + Name(gitHubIdentityProvider.Name). + SubResource("status"). + VersionedParams(&opts, scheme.ParameterCodec). + Body(gitHubIdentityProvider). + Do(ctx). + Into(result) + return +} + +// Delete takes name of the gitHubIdentityProvider and deletes it. Returns an error if one occurs. +func (c *gitHubIdentityProviders) Delete(ctx context.Context, name string, opts v1.DeleteOptions) error { + return c.client.Delete(). + Namespace(c.ns). + Resource("githubidentityproviders"). + Name(name). + Body(&opts). + Do(ctx). + Error() +} + +// DeleteCollection deletes a collection of objects. +func (c *gitHubIdentityProviders) DeleteCollection(ctx context.Context, opts v1.DeleteOptions, listOpts v1.ListOptions) error { + var timeout time.Duration + if listOpts.TimeoutSeconds != nil { + timeout = time.Duration(*listOpts.TimeoutSeconds) * time.Second + } + return c.client.Delete(). + Namespace(c.ns). + Resource("githubidentityproviders"). + VersionedParams(&listOpts, scheme.ParameterCodec). + Timeout(timeout). + Body(&opts). + Do(ctx). + Error() +} + +// Patch applies the patch and returns the patched gitHubIdentityProvider. +func (c *gitHubIdentityProviders) Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts v1.PatchOptions, subresources ...string) (result *v1alpha1.GitHubIdentityProvider, err error) { + result = &v1alpha1.GitHubIdentityProvider{} + err = c.client.Patch(pt). + Namespace(c.ns). + Resource("githubidentityproviders"). + Name(name). + SubResource(subresources...). + VersionedParams(&opts, scheme.ParameterCodec). + Body(data). + Do(ctx). + Into(result) + return +} diff --git a/generated/1.27/client/supervisor/clientset/versioned/typed/idp/v1alpha1/idp_client.go b/generated/1.27/client/supervisor/clientset/versioned/typed/idp/v1alpha1/idp_client.go index c9eec1efd..48afb2a22 100644 --- a/generated/1.27/client/supervisor/clientset/versioned/typed/idp/v1alpha1/idp_client.go +++ b/generated/1.27/client/supervisor/clientset/versioned/typed/idp/v1alpha1/idp_client.go @@ -16,6 +16,7 @@ import ( type IDPV1alpha1Interface interface { RESTClient() rest.Interface ActiveDirectoryIdentityProvidersGetter + GitHubIdentityProvidersGetter LDAPIdentityProvidersGetter OIDCIdentityProvidersGetter } @@ -29,6 +30,10 @@ func (c *IDPV1alpha1Client) ActiveDirectoryIdentityProviders(namespace string) A return newActiveDirectoryIdentityProviders(c, namespace) } +func (c *IDPV1alpha1Client) GitHubIdentityProviders(namespace string) GitHubIdentityProviderInterface { + return newGitHubIdentityProviders(c, namespace) +} + func (c *IDPV1alpha1Client) LDAPIdentityProviders(namespace string) LDAPIdentityProviderInterface { return newLDAPIdentityProviders(c, namespace) } diff --git a/generated/1.27/client/supervisor/informers/externalversions/generic.go b/generated/1.27/client/supervisor/informers/externalversions/generic.go index 535476ff3..72a959fb9 100644 --- a/generated/1.27/client/supervisor/informers/externalversions/generic.go +++ b/generated/1.27/client/supervisor/informers/externalversions/generic.go @@ -49,6 +49,8 @@ func (f *sharedInformerFactory) ForResource(resource schema.GroupVersionResource // Group=idp.supervisor.pinniped.dev, Version=v1alpha1 case idpv1alpha1.SchemeGroupVersion.WithResource("activedirectoryidentityproviders"): return &genericInformer{resource: resource.GroupResource(), informer: f.IDP().V1alpha1().ActiveDirectoryIdentityProviders().Informer()}, nil + case idpv1alpha1.SchemeGroupVersion.WithResource("githubidentityproviders"): + return &genericInformer{resource: resource.GroupResource(), informer: f.IDP().V1alpha1().GitHubIdentityProviders().Informer()}, nil case idpv1alpha1.SchemeGroupVersion.WithResource("ldapidentityproviders"): return &genericInformer{resource: resource.GroupResource(), informer: f.IDP().V1alpha1().LDAPIdentityProviders().Informer()}, nil case idpv1alpha1.SchemeGroupVersion.WithResource("oidcidentityproviders"): diff --git a/generated/1.27/client/supervisor/informers/externalversions/idp/v1alpha1/githubidentityprovider.go b/generated/1.27/client/supervisor/informers/externalversions/idp/v1alpha1/githubidentityprovider.go new file mode 100644 index 000000000..b65004f2c --- /dev/null +++ b/generated/1.27/client/supervisor/informers/externalversions/idp/v1alpha1/githubidentityprovider.go @@ -0,0 +1,77 @@ +// Copyright 2020-2024 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +// Code generated by informer-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + "context" + time "time" + + idpv1alpha1 "go.pinniped.dev/generated/1.27/apis/supervisor/idp/v1alpha1" + versioned "go.pinniped.dev/generated/1.27/client/supervisor/clientset/versioned" + internalinterfaces "go.pinniped.dev/generated/1.27/client/supervisor/informers/externalversions/internalinterfaces" + v1alpha1 "go.pinniped.dev/generated/1.27/client/supervisor/listers/idp/v1alpha1" + 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" +) + +// GitHubIdentityProviderInformer provides access to a shared informer and lister for +// GitHubIdentityProviders. +type GitHubIdentityProviderInformer interface { + Informer() cache.SharedIndexInformer + Lister() v1alpha1.GitHubIdentityProviderLister +} + +type gitHubIdentityProviderInformer struct { + factory internalinterfaces.SharedInformerFactory + tweakListOptions internalinterfaces.TweakListOptionsFunc + namespace string +} + +// NewGitHubIdentityProviderInformer constructs a new informer for GitHubIdentityProvider 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 NewGitHubIdentityProviderInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers) cache.SharedIndexInformer { + return NewFilteredGitHubIdentityProviderInformer(client, namespace, resyncPeriod, indexers, nil) +} + +// NewFilteredGitHubIdentityProviderInformer constructs a new informer for GitHubIdentityProvider 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 NewFilteredGitHubIdentityProviderInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers, tweakListOptions internalinterfaces.TweakListOptionsFunc) cache.SharedIndexInformer { + return cache.NewSharedIndexInformer( + &cache.ListWatch{ + ListFunc: func(options v1.ListOptions) (runtime.Object, error) { + if tweakListOptions != nil { + tweakListOptions(&options) + } + return client.IDPV1alpha1().GitHubIdentityProviders(namespace).List(context.TODO(), options) + }, + WatchFunc: func(options v1.ListOptions) (watch.Interface, error) { + if tweakListOptions != nil { + tweakListOptions(&options) + } + return client.IDPV1alpha1().GitHubIdentityProviders(namespace).Watch(context.TODO(), options) + }, + }, + &idpv1alpha1.GitHubIdentityProvider{}, + resyncPeriod, + indexers, + ) +} + +func (f *gitHubIdentityProviderInformer) defaultInformer(client versioned.Interface, resyncPeriod time.Duration) cache.SharedIndexInformer { + return NewFilteredGitHubIdentityProviderInformer(client, f.namespace, resyncPeriod, cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc}, f.tweakListOptions) +} + +func (f *gitHubIdentityProviderInformer) Informer() cache.SharedIndexInformer { + return f.factory.InformerFor(&idpv1alpha1.GitHubIdentityProvider{}, f.defaultInformer) +} + +func (f *gitHubIdentityProviderInformer) Lister() v1alpha1.GitHubIdentityProviderLister { + return v1alpha1.NewGitHubIdentityProviderLister(f.Informer().GetIndexer()) +} diff --git a/generated/1.27/client/supervisor/informers/externalversions/idp/v1alpha1/interface.go b/generated/1.27/client/supervisor/informers/externalversions/idp/v1alpha1/interface.go index 1dd455411..3466abdd5 100644 --- a/generated/1.27/client/supervisor/informers/externalversions/idp/v1alpha1/interface.go +++ b/generated/1.27/client/supervisor/informers/externalversions/idp/v1alpha1/interface.go @@ -13,6 +13,8 @@ import ( type Interface interface { // ActiveDirectoryIdentityProviders returns a ActiveDirectoryIdentityProviderInformer. ActiveDirectoryIdentityProviders() ActiveDirectoryIdentityProviderInformer + // GitHubIdentityProviders returns a GitHubIdentityProviderInformer. + GitHubIdentityProviders() GitHubIdentityProviderInformer // LDAPIdentityProviders returns a LDAPIdentityProviderInformer. LDAPIdentityProviders() LDAPIdentityProviderInformer // OIDCIdentityProviders returns a OIDCIdentityProviderInformer. @@ -35,6 +37,11 @@ func (v *version) ActiveDirectoryIdentityProviders() ActiveDirectoryIdentityProv return &activeDirectoryIdentityProviderInformer{factory: v.factory, namespace: v.namespace, tweakListOptions: v.tweakListOptions} } +// GitHubIdentityProviders returns a GitHubIdentityProviderInformer. +func (v *version) GitHubIdentityProviders() GitHubIdentityProviderInformer { + return &gitHubIdentityProviderInformer{factory: v.factory, namespace: v.namespace, tweakListOptions: v.tweakListOptions} +} + // LDAPIdentityProviders returns a LDAPIdentityProviderInformer. func (v *version) LDAPIdentityProviders() LDAPIdentityProviderInformer { return &lDAPIdentityProviderInformer{factory: v.factory, namespace: v.namespace, tweakListOptions: v.tweakListOptions} diff --git a/generated/1.27/client/supervisor/listers/idp/v1alpha1/expansion_generated.go b/generated/1.27/client/supervisor/listers/idp/v1alpha1/expansion_generated.go index b491a9e8d..470c06abb 100644 --- a/generated/1.27/client/supervisor/listers/idp/v1alpha1/expansion_generated.go +++ b/generated/1.27/client/supervisor/listers/idp/v1alpha1/expansion_generated.go @@ -13,6 +13,14 @@ type ActiveDirectoryIdentityProviderListerExpansion interface{} // ActiveDirectoryIdentityProviderNamespaceLister. type ActiveDirectoryIdentityProviderNamespaceListerExpansion interface{} +// GitHubIdentityProviderListerExpansion allows custom methods to be added to +// GitHubIdentityProviderLister. +type GitHubIdentityProviderListerExpansion interface{} + +// GitHubIdentityProviderNamespaceListerExpansion allows custom methods to be added to +// GitHubIdentityProviderNamespaceLister. +type GitHubIdentityProviderNamespaceListerExpansion interface{} + // LDAPIdentityProviderListerExpansion allows custom methods to be added to // LDAPIdentityProviderLister. type LDAPIdentityProviderListerExpansion interface{} diff --git a/generated/1.27/client/supervisor/listers/idp/v1alpha1/githubidentityprovider.go b/generated/1.27/client/supervisor/listers/idp/v1alpha1/githubidentityprovider.go new file mode 100644 index 000000000..8cb38e379 --- /dev/null +++ b/generated/1.27/client/supervisor/listers/idp/v1alpha1/githubidentityprovider.go @@ -0,0 +1,86 @@ +// Copyright 2020-2024 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +// Code generated by lister-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + v1alpha1 "go.pinniped.dev/generated/1.27/apis/supervisor/idp/v1alpha1" + "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/client-go/tools/cache" +) + +// GitHubIdentityProviderLister helps list GitHubIdentityProviders. +// All objects returned here must be treated as read-only. +type GitHubIdentityProviderLister interface { + // List lists all GitHubIdentityProviders in the indexer. + // Objects returned here must be treated as read-only. + List(selector labels.Selector) (ret []*v1alpha1.GitHubIdentityProvider, err error) + // GitHubIdentityProviders returns an object that can list and get GitHubIdentityProviders. + GitHubIdentityProviders(namespace string) GitHubIdentityProviderNamespaceLister + GitHubIdentityProviderListerExpansion +} + +// gitHubIdentityProviderLister implements the GitHubIdentityProviderLister interface. +type gitHubIdentityProviderLister struct { + indexer cache.Indexer +} + +// NewGitHubIdentityProviderLister returns a new GitHubIdentityProviderLister. +func NewGitHubIdentityProviderLister(indexer cache.Indexer) GitHubIdentityProviderLister { + return &gitHubIdentityProviderLister{indexer: indexer} +} + +// List lists all GitHubIdentityProviders in the indexer. +func (s *gitHubIdentityProviderLister) List(selector labels.Selector) (ret []*v1alpha1.GitHubIdentityProvider, err error) { + err = cache.ListAll(s.indexer, selector, func(m interface{}) { + ret = append(ret, m.(*v1alpha1.GitHubIdentityProvider)) + }) + return ret, err +} + +// GitHubIdentityProviders returns an object that can list and get GitHubIdentityProviders. +func (s *gitHubIdentityProviderLister) GitHubIdentityProviders(namespace string) GitHubIdentityProviderNamespaceLister { + return gitHubIdentityProviderNamespaceLister{indexer: s.indexer, namespace: namespace} +} + +// GitHubIdentityProviderNamespaceLister helps list and get GitHubIdentityProviders. +// All objects returned here must be treated as read-only. +type GitHubIdentityProviderNamespaceLister interface { + // List lists all GitHubIdentityProviders in the indexer for a given namespace. + // Objects returned here must be treated as read-only. + List(selector labels.Selector) (ret []*v1alpha1.GitHubIdentityProvider, err error) + // Get retrieves the GitHubIdentityProvider from the indexer for a given namespace and name. + // Objects returned here must be treated as read-only. + Get(name string) (*v1alpha1.GitHubIdentityProvider, error) + GitHubIdentityProviderNamespaceListerExpansion +} + +// gitHubIdentityProviderNamespaceLister implements the GitHubIdentityProviderNamespaceLister +// interface. +type gitHubIdentityProviderNamespaceLister struct { + indexer cache.Indexer + namespace string +} + +// List lists all GitHubIdentityProviders in the indexer for a given namespace. +func (s gitHubIdentityProviderNamespaceLister) List(selector labels.Selector) (ret []*v1alpha1.GitHubIdentityProvider, err error) { + err = cache.ListAllByNamespace(s.indexer, s.namespace, selector, func(m interface{}) { + ret = append(ret, m.(*v1alpha1.GitHubIdentityProvider)) + }) + return ret, err +} + +// Get retrieves the GitHubIdentityProvider from the indexer for a given namespace and name. +func (s gitHubIdentityProviderNamespaceLister) Get(name string) (*v1alpha1.GitHubIdentityProvider, error) { + obj, exists, err := s.indexer.GetByKey(s.namespace + "/" + name) + if err != nil { + return nil, err + } + if !exists { + return nil, errors.NewNotFound(v1alpha1.Resource("githubidentityprovider"), name) + } + return obj.(*v1alpha1.GitHubIdentityProvider), nil +} diff --git a/generated/1.27/crds/authentication.concierge.pinniped.dev_jwtauthenticators.yaml b/generated/1.27/crds/authentication.concierge.pinniped.dev_jwtauthenticators.yaml index b464cfa2b..d59fcb783 100644 --- a/generated/1.27/crds/authentication.concierge.pinniped.dev_jwtauthenticators.yaml +++ b/generated/1.27/crds/authentication.concierge.pinniped.dev_jwtauthenticators.yaml @@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - controller-gen.kubebuilder.io/version: v0.14.0 + controller-gen.kubebuilder.io/version: v0.15.0 name: jwtauthenticators.authentication.concierge.pinniped.dev spec: group: authentication.concierge.pinniped.dev diff --git a/generated/1.27/crds/authentication.concierge.pinniped.dev_webhookauthenticators.yaml b/generated/1.27/crds/authentication.concierge.pinniped.dev_webhookauthenticators.yaml index e8cbc6790..4ccd53770 100644 --- a/generated/1.27/crds/authentication.concierge.pinniped.dev_webhookauthenticators.yaml +++ b/generated/1.27/crds/authentication.concierge.pinniped.dev_webhookauthenticators.yaml @@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - controller-gen.kubebuilder.io/version: v0.14.0 + controller-gen.kubebuilder.io/version: v0.15.0 name: webhookauthenticators.authentication.concierge.pinniped.dev spec: group: authentication.concierge.pinniped.dev diff --git a/generated/1.27/crds/config.concierge.pinniped.dev_credentialissuers.yaml b/generated/1.27/crds/config.concierge.pinniped.dev_credentialissuers.yaml index 8d77a6665..26fed7376 100644 --- a/generated/1.27/crds/config.concierge.pinniped.dev_credentialissuers.yaml +++ b/generated/1.27/crds/config.concierge.pinniped.dev_credentialissuers.yaml @@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - controller-gen.kubebuilder.io/version: v0.14.0 + controller-gen.kubebuilder.io/version: v0.15.0 name: credentialissuers.config.concierge.pinniped.dev spec: group: config.concierge.pinniped.dev diff --git a/generated/1.27/crds/config.supervisor.pinniped.dev_federationdomains.yaml b/generated/1.27/crds/config.supervisor.pinniped.dev_federationdomains.yaml index 7bf231443..d43c7406d 100644 --- a/generated/1.27/crds/config.supervisor.pinniped.dev_federationdomains.yaml +++ b/generated/1.27/crds/config.supervisor.pinniped.dev_federationdomains.yaml @@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - controller-gen.kubebuilder.io/version: v0.14.0 + controller-gen.kubebuilder.io/version: v0.15.0 name: federationdomains.config.supervisor.pinniped.dev spec: group: config.supervisor.pinniped.dev diff --git a/generated/1.27/crds/config.supervisor.pinniped.dev_oidcclients.yaml b/generated/1.27/crds/config.supervisor.pinniped.dev_oidcclients.yaml index d33b31bf1..c44ecbf1e 100644 --- a/generated/1.27/crds/config.supervisor.pinniped.dev_oidcclients.yaml +++ b/generated/1.27/crds/config.supervisor.pinniped.dev_oidcclients.yaml @@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - controller-gen.kubebuilder.io/version: v0.14.0 + controller-gen.kubebuilder.io/version: v0.15.0 name: oidcclients.config.supervisor.pinniped.dev spec: group: config.supervisor.pinniped.dev diff --git a/generated/1.27/crds/idp.supervisor.pinniped.dev_activedirectoryidentityproviders.yaml b/generated/1.27/crds/idp.supervisor.pinniped.dev_activedirectoryidentityproviders.yaml index 414ac4f51..062251102 100644 --- a/generated/1.27/crds/idp.supervisor.pinniped.dev_activedirectoryidentityproviders.yaml +++ b/generated/1.27/crds/idp.supervisor.pinniped.dev_activedirectoryidentityproviders.yaml @@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - controller-gen.kubebuilder.io/version: v0.14.0 + controller-gen.kubebuilder.io/version: v0.15.0 name: activedirectoryidentityproviders.idp.supervisor.pinniped.dev spec: group: idp.supervisor.pinniped.dev diff --git a/generated/1.27/crds/idp.supervisor.pinniped.dev_githubidentityproviders.yaml b/generated/1.27/crds/idp.supervisor.pinniped.dev_githubidentityproviders.yaml new file mode 100644 index 000000000..f93108700 --- /dev/null +++ b/generated/1.27/crds/idp.supervisor.pinniped.dev_githubidentityproviders.yaml @@ -0,0 +1,326 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.15.0 + name: githubidentityproviders.idp.supervisor.pinniped.dev +spec: + group: idp.supervisor.pinniped.dev + names: + categories: + - pinniped + - pinniped-idp + - pinniped-idps + kind: GitHubIdentityProvider + listKind: GitHubIdentityProviderList + plural: githubidentityproviders + singular: githubidentityprovider + scope: Namespaced + versions: + - additionalPrinterColumns: + - jsonPath: .spec.githubAPI.host + name: Host + type: string + - jsonPath: .status.phase + name: Status + type: string + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1alpha1 + schema: + openAPIV3Schema: + description: |- + GitHubIdentityProvider describes the configuration of an upstream GitHub identity provider. + This upstream provider can be configured with either a GitHub App or a GitHub OAuth2 App. + + + Right now, only web-based logins are supported, for both the pinniped-cli client and clients configured + as OIDCClients. + 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: + description: Spec for configuring the identity provider. + properties: + allowAuthentication: + description: AllowAuthentication allows customization of who can authenticate + using this IDP and how. + properties: + organizations: + description: Organizations allows customization of which organizations + can authenticate using this IDP. + properties: + allowed: + description: |- + Allowed, when specified, indicates that only users with membership in at least one of the listed + GitHub organizations may log in. In addition, the group membership presented to Kubernetes will only include + teams within the listed GitHub organizations. Additional login rules or group filtering can optionally be + provided as policy expression on any Pinniped Supervisor FederationDomain that includes this IDP. + + + The configured GitHub App or GitHub OAuth App must be allowed to see membership in the listed organizations, + otherwise Pinniped will not be aware that the user belongs to the listed organization or any teams + within that organization. + + + If no organizations are listed, you must set organizations: AllGitHubUsers. + items: + type: string + maxItems: 64 + type: array + x-kubernetes-list-type: set + policy: + default: OnlyUsersFromAllowedOrganizations + description: |- + Policy must be set to "AllGitHubUsers" if allowed is empty. + + + This field only exists to ensure that Pinniped administrators are aware that an empty list of + allowedOrganizations means all GitHub users are allowed to log in. + enum: + - OnlyUsersFromAllowedOrganizations + - AllGitHubUsers + type: string + type: object + x-kubernetes-validations: + - message: spec.allowAuthentication.organizations.policy must + be 'OnlyUsersFromAllowedOrganizations' when spec.allowAuthentication.organizations.allowed + has organizations listed + rule: '!(has(self.allowed) && size(self.allowed) > 0 && self.policy + == ''AllGitHubUsers'')' + - message: spec.allowAuthentication.organizations.policy must + be 'AllGitHubUsers' when spec.allowAuthentication.organizations.allowed + is empty + rule: '!((!has(self.allowed) || size(self.allowed) == 0) && + self.policy == ''OnlyUsersFromAllowedOrganizations'')' + required: + - organizations + type: object + claims: + default: {} + description: Claims allows customization of the username and groups + claims. + properties: + groups: + default: slug + description: |- + Groups configures which property of the GitHub team record shall determine the group names in Kubernetes. + + + Can be either "name" or "slug". Defaults to "slug". + + + GitHub team names can contain upper and lower case characters, whitespace, and punctuation (e.g. "Kube admins!"). + + + GitHub team slugs are lower case alphanumeric characters and may contain dashes and underscores (e.g. "kube-admins"). + + + Group names as presented to Kubernetes will always be prefixed by the GitHub organization name followed by a + forward slash (e.g. "my-org/my-team"). GitHub organization login names can only contain alphanumeric characters + or single hyphens, so the first forward slash `/` will be the separator between the organization login name and + the team name or slug. + + + If desired, an admin could configure identity transformation expressions on the Pinniped Supervisor's + FederationDomain to further customize how these group names are presented to Kubernetes. + + + See the response schema for + [List teams for the authenticated user](https://docs.github.com/en/rest/teams/teams?apiVersion=2022-11-28#list-teams-for-the-authenticated-user). + enum: + - name + - slug + type: string + username: + default: login:id + description: |- + Username configures which property of the GitHub user record shall determine the username in Kubernetes. + + + Can be either "id", "login", or "login:id". Defaults to "login:id". + + + GitHub's user login attributes can only contain alphanumeric characters and non-repeating hyphens, + and may not start or end with hyphens. GitHub users are allowed to change their login name, + although it is inconvenient. If a GitHub user changed their login name from "foo" to "bar", + then a second user might change their name from "baz" to "foo" in order to take the old + username of the first user. For this reason, it is not as safe to make authorization decisions + based only on the user's login attribute. + + + If desired, an admin could configure identity transformation expressions on the Pinniped Supervisor's + FederationDomain to further customize how these usernames are presented to Kubernetes. + + + Defaults to "login:id", which is the user login attribute, followed by a colon, followed by the unique and + unchanging integer ID number attribute. This blends human-readable login names with the unchanging ID value + from GitHub. Colons are not allowed in GitHub login attributes or ID numbers, so this is a reasonable + choice to concatenate the two values. + + + See the response schema for + [Get the authenticated user](https://docs.github.com/en/rest/users/users?apiVersion=2022-11-28#get-the-authenticated-user). + enum: + - id + - login + - login:id + type: string + type: object + client: + description: Client identifies the secret with credentials for a GitHub + App or GitHub OAuth2 App (a GitHub client). + properties: + secretName: + description: |- + SecretName contains the name of a namespace-local Secret object that provides the clientID and + clientSecret for an GitHub App or GitHub OAuth2 client. + + + This secret must be of type "secrets.pinniped.dev/github-client" with keys "clientID" and "clientSecret". + minLength: 1 + type: string + required: + - secretName + type: object + githubAPI: + default: {} + description: GitHubAPI allows configuration for GitHub Enterprise + Server + properties: + host: + default: github.com + description: |- + Host is required only for GitHub Enterprise Server. + Defaults to using GitHub's public API ("github.com"). + Do not specify a protocol or scheme since "https://" will always be used. + Port is optional. Do not specify a path, query, fragment, or userinfo. + Only domain name or IP address, subdomains (optional), and port (optional). + IPv4 and IPv6 are supported. If using an IPv6 address with a port, you must enclose the IPv6 address + in square brackets. Example: "[::1]:443". + minLength: 1 + type: string + tls: + description: TLS configuration for GitHub Enterprise Server. + properties: + certificateAuthorityData: + description: X.509 Certificate Authority (base64-encoded PEM + bundle). If omitted, a default set of system roots will + be trusted. + type: string + type: object + type: object + required: + - allowAuthentication + - client + type: object + status: + description: Status of the identity provider. + properties: + conditions: + description: Conditions represents the observations of an identity + provider's current state. + items: + description: "Condition contains details for one aspect of the current + state of this API Resource.\n---\nThis struct is intended for + direct use as an array at the field path .status.conditions. For + example,\n\n\n\ttype FooStatus struct{\n\t // Represents the + observations of a foo's current state.\n\t // Known .status.conditions.type + are: \"Available\", \"Progressing\", and \"Degraded\"\n\t // + +patchMergeKey=type\n\t // +patchStrategy=merge\n\t // +listType=map\n\t + \ // +listMapKey=type\n\t Conditions []metav1.Condition `json:\"conditions,omitempty\" + patchStrategy:\"merge\" patchMergeKey:\"type\" protobuf:\"bytes,1,rep,name=conditions\"`\n\n\n\t + \ // other fields\n\t}" + properties: + lastTransitionTime: + description: |- + lastTransitionTime is the last time the condition transitioned from one status to another. + This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: |- + message is a human readable message indicating details about the transition. + This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: |- + observedGeneration represents the .metadata.generation that the condition was set based upon. + For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date + with respect to the current state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: |- + reason contains a programmatic identifier indicating the reason for the condition's last transition. + Producers of specific condition types may define expected values and meanings for this field, + and whether the values are considered a guaranteed API. + The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: |- + type of condition in CamelCase or in foo.example.com/CamelCase. + --- + Many .condition.type values are consistent across resources like Available, but because arbitrary conditions can be + useful (see .node.status.conditions), the ability to deconflict is important. + The regex it matches is (dns1123SubdomainFmt/)?(qualifiedNameFmt) + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + x-kubernetes-list-map-keys: + - type + x-kubernetes-list-type: map + phase: + default: Pending + description: Phase summarizes the overall status of the GitHubIdentityProvider. + enum: + - Pending + - Ready + - Error + type: string + type: object + required: + - spec + type: object + served: true + storage: true + subresources: + status: {} diff --git a/generated/1.27/crds/idp.supervisor.pinniped.dev_ldapidentityproviders.yaml b/generated/1.27/crds/idp.supervisor.pinniped.dev_ldapidentityproviders.yaml index f718e93aa..711e9a754 100644 --- a/generated/1.27/crds/idp.supervisor.pinniped.dev_ldapidentityproviders.yaml +++ b/generated/1.27/crds/idp.supervisor.pinniped.dev_ldapidentityproviders.yaml @@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - controller-gen.kubebuilder.io/version: v0.14.0 + controller-gen.kubebuilder.io/version: v0.15.0 name: ldapidentityproviders.idp.supervisor.pinniped.dev spec: group: idp.supervisor.pinniped.dev diff --git a/generated/1.27/crds/idp.supervisor.pinniped.dev_oidcidentityproviders.yaml b/generated/1.27/crds/idp.supervisor.pinniped.dev_oidcidentityproviders.yaml index 486665605..acfca1573 100644 --- a/generated/1.27/crds/idp.supervisor.pinniped.dev_oidcidentityproviders.yaml +++ b/generated/1.27/crds/idp.supervisor.pinniped.dev_oidcidentityproviders.yaml @@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - controller-gen.kubebuilder.io/version: v0.14.0 + controller-gen.kubebuilder.io/version: v0.15.0 name: oidcidentityproviders.idp.supervisor.pinniped.dev spec: group: idp.supervisor.pinniped.dev diff --git a/generated/1.28/README.adoc b/generated/1.28/README.adoc index 93cc1a927..fb7366e47 100644 --- a/generated/1.28/README.adoc +++ b/generated/1.28/README.adoc @@ -1645,6 +1645,285 @@ Optional, when empty this defaults to "objectGUID". + |=== +[id="{anchor_prefix}-go-pinniped-dev-generated-1-28-apis-supervisor-idp-v1alpha1-githubapiconfig"] +==== GitHubAPIConfig + +GitHubAPIConfig allows configuration for GitHub Enterprise Server + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-28-apis-supervisor-idp-v1alpha1-githubidentityproviderspec[$$GitHubIdentityProviderSpec$$] +**** + +[cols="25a,75a", options="header"] +|=== +| Field | Description +| *`host`* __string__ | Host is required only for GitHub Enterprise Server. + +Defaults to using GitHub's public API ("github.com"). + +Do not specify a protocol or scheme since "https://" will always be used. + +Port is optional. Do not specify a path, query, fragment, or userinfo. + +Only domain name or IP address, subdomains (optional), and port (optional). + +IPv4 and IPv6 are supported. If using an IPv6 address with a port, you must enclose the IPv6 address + +in square brackets. Example: "[::1]:443". + +| *`tls`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-28-apis-supervisor-idp-v1alpha1-tlsspec[$$TLSSpec$$]__ | TLS configuration for GitHub Enterprise Server. + +|=== + + +[id="{anchor_prefix}-go-pinniped-dev-generated-1-28-apis-supervisor-idp-v1alpha1-githuballowauthenticationspec"] +==== GitHubAllowAuthenticationSpec + +GitHubAllowAuthenticationSpec allows customization of who can authenticate using this IDP and how. + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-28-apis-supervisor-idp-v1alpha1-githubidentityproviderspec[$$GitHubIdentityProviderSpec$$] +**** + +[cols="25a,75a", options="header"] +|=== +| Field | Description +| *`organizations`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-28-apis-supervisor-idp-v1alpha1-githuborganizationsspec[$$GitHubOrganizationsSpec$$]__ | Organizations allows customization of which organizations can authenticate using this IDP. + +|=== + + +[id="{anchor_prefix}-go-pinniped-dev-generated-1-28-apis-supervisor-idp-v1alpha1-githuballowedauthorganizationspolicy"] +==== GitHubAllowedAuthOrganizationsPolicy (string) + + + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-28-apis-supervisor-idp-v1alpha1-githuborganizationsspec[$$GitHubOrganizationsSpec$$] +**** + + + +[id="{anchor_prefix}-go-pinniped-dev-generated-1-28-apis-supervisor-idp-v1alpha1-githubclaims"] +==== GitHubClaims + +GitHubClaims allows customization of the username and groups claims. + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-28-apis-supervisor-idp-v1alpha1-githubidentityproviderspec[$$GitHubIdentityProviderSpec$$] +**** + +[cols="25a,75a", options="header"] +|=== +| Field | Description +| *`username`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-28-apis-supervisor-idp-v1alpha1-githubusernameattribute[$$GitHubUsernameAttribute$$]__ | Username configures which property of the GitHub user record shall determine the username in Kubernetes. + + + +Can be either "id", "login", or "login:id". Defaults to "login:id". + + + +GitHub's user login attributes can only contain alphanumeric characters and non-repeating hyphens, + +and may not start or end with hyphens. GitHub users are allowed to change their login name, + +although it is inconvenient. If a GitHub user changed their login name from "foo" to "bar", + +then a second user might change their name from "baz" to "foo" in order to take the old + +username of the first user. For this reason, it is not as safe to make authorization decisions + +based only on the user's login attribute. + + + +If desired, an admin could configure identity transformation expressions on the Pinniped Supervisor's + +FederationDomain to further customize how these usernames are presented to Kubernetes. + + + +Defaults to "login:id", which is the user login attribute, followed by a colon, followed by the unique and + +unchanging integer ID number attribute. This blends human-readable login names with the unchanging ID value + +from GitHub. Colons are not allowed in GitHub login attributes or ID numbers, so this is a reasonable + +choice to concatenate the two values. + + + +See the response schema for + +[Get the authenticated user](https://docs.github.com/en/rest/users/users?apiVersion=2022-11-28#get-the-authenticated-user). + +| *`groups`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-28-apis-supervisor-idp-v1alpha1-githubgroupnameattribute[$$GitHubGroupNameAttribute$$]__ | Groups configures which property of the GitHub team record shall determine the group names in Kubernetes. + + + +Can be either "name" or "slug". Defaults to "slug". + + + +GitHub team names can contain upper and lower case characters, whitespace, and punctuation (e.g. "Kube admins!"). + + + +GitHub team slugs are lower case alphanumeric characters and may contain dashes and underscores (e.g. "kube-admins"). + + + +Group names as presented to Kubernetes will always be prefixed by the GitHub organization name followed by a + +forward slash (e.g. "my-org/my-team"). GitHub organization login names can only contain alphanumeric characters + +or single hyphens, so the first forward slash `/` will be the separator between the organization login name and + +the team name or slug. + + + +If desired, an admin could configure identity transformation expressions on the Pinniped Supervisor's + +FederationDomain to further customize how these group names are presented to Kubernetes. + + + +See the response schema for + +[List teams for the authenticated user](https://docs.github.com/en/rest/teams/teams?apiVersion=2022-11-28#list-teams-for-the-authenticated-user). + +|=== + + +[id="{anchor_prefix}-go-pinniped-dev-generated-1-28-apis-supervisor-idp-v1alpha1-githubclientspec"] +==== GitHubClientSpec + +GitHubClientSpec contains information about the GitHub client that this identity provider will use +for web-based login flows. + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-28-apis-supervisor-idp-v1alpha1-githubidentityproviderspec[$$GitHubIdentityProviderSpec$$] +**** + +[cols="25a,75a", options="header"] +|=== +| Field | Description +| *`secretName`* __string__ | SecretName contains the name of a namespace-local Secret object that provides the clientID and + +clientSecret for an GitHub App or GitHub OAuth2 client. + + + +This secret must be of type "secrets.pinniped.dev/github-client" with keys "clientID" and "clientSecret". + +|=== + + +[id="{anchor_prefix}-go-pinniped-dev-generated-1-28-apis-supervisor-idp-v1alpha1-githubgroupnameattribute"] +==== GitHubGroupNameAttribute (string) + +GitHubGroupNameAttribute allows the user to specify which attribute from GitHub to use for the group +names to present to Kubernetes. See the response schema for +[List teams for the authenticated user](https://docs.github.com/en/rest/teams/teams?apiVersion=2022-11-28#list-teams-for-the-authenticated-user). + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-28-apis-supervisor-idp-v1alpha1-githubclaims[$$GitHubClaims$$] +**** + + + +[id="{anchor_prefix}-go-pinniped-dev-generated-1-28-apis-supervisor-idp-v1alpha1-githubidentityprovider"] +==== GitHubIdentityProvider + +GitHubIdentityProvider describes the configuration of an upstream GitHub identity provider. +This upstream provider can be configured with either a GitHub App or a GitHub OAuth2 App. + + +Right now, only web-based logins are supported, for both the pinniped-cli client and clients configured +as OIDCClients. + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-28-apis-supervisor-idp-v1alpha1-githubidentityproviderlist[$$GitHubIdentityProviderList$$] +**** + +[cols="25a,75a", options="header"] +|=== +| Field | Description +| *`metadata`* __link:https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.28/#objectmeta-v1-meta[$$ObjectMeta$$]__ | Refer to Kubernetes API documentation for fields of `metadata`. + +| *`spec`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-28-apis-supervisor-idp-v1alpha1-githubidentityproviderspec[$$GitHubIdentityProviderSpec$$]__ | Spec for configuring the identity provider. + +| *`status`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-28-apis-supervisor-idp-v1alpha1-githubidentityproviderstatus[$$GitHubIdentityProviderStatus$$]__ | Status of the identity provider. + +|=== + + + + +[id="{anchor_prefix}-go-pinniped-dev-generated-1-28-apis-supervisor-idp-v1alpha1-githubidentityproviderphase"] +==== GitHubIdentityProviderPhase (string) + + + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-28-apis-supervisor-idp-v1alpha1-githubidentityproviderstatus[$$GitHubIdentityProviderStatus$$] +**** + + + +[id="{anchor_prefix}-go-pinniped-dev-generated-1-28-apis-supervisor-idp-v1alpha1-githubidentityproviderspec"] +==== GitHubIdentityProviderSpec + +GitHubIdentityProviderSpec is the spec for configuring an GitHub identity provider. + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-28-apis-supervisor-idp-v1alpha1-githubidentityprovider[$$GitHubIdentityProvider$$] +**** + +[cols="25a,75a", options="header"] +|=== +| Field | Description +| *`githubAPI`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-28-apis-supervisor-idp-v1alpha1-githubapiconfig[$$GitHubAPIConfig$$]__ | GitHubAPI allows configuration for GitHub Enterprise Server + +| *`claims`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-28-apis-supervisor-idp-v1alpha1-githubclaims[$$GitHubClaims$$]__ | Claims allows customization of the username and groups claims. + +| *`allowAuthentication`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-28-apis-supervisor-idp-v1alpha1-githuballowauthenticationspec[$$GitHubAllowAuthenticationSpec$$]__ | AllowAuthentication allows customization of who can authenticate using this IDP and how. + +| *`client`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-28-apis-supervisor-idp-v1alpha1-githubclientspec[$$GitHubClientSpec$$]__ | Client identifies the secret with credentials for a GitHub App or GitHub OAuth2 App (a GitHub client). + +|=== + + +[id="{anchor_prefix}-go-pinniped-dev-generated-1-28-apis-supervisor-idp-v1alpha1-githubidentityproviderstatus"] +==== GitHubIdentityProviderStatus + +GitHubIdentityProviderStatus is the status of an GitHub identity provider. + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-28-apis-supervisor-idp-v1alpha1-githubidentityprovider[$$GitHubIdentityProvider$$] +**** + +[cols="25a,75a", options="header"] +|=== +| Field | Description +| *`phase`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-28-apis-supervisor-idp-v1alpha1-githubidentityproviderphase[$$GitHubIdentityProviderPhase$$]__ | Phase summarizes the overall status of the GitHubIdentityProvider. + +| *`conditions`* __link:https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.28/#condition-v1-meta[$$Condition$$] array__ | Conditions represents the observations of an identity provider's current state. + +|=== + + +[id="{anchor_prefix}-go-pinniped-dev-generated-1-28-apis-supervisor-idp-v1alpha1-githuborganizationsspec"] +==== GitHubOrganizationsSpec + + + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-28-apis-supervisor-idp-v1alpha1-githuballowauthenticationspec[$$GitHubAllowAuthenticationSpec$$] +**** + +[cols="25a,75a", options="header"] +|=== +| Field | Description +| *`policy`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-28-apis-supervisor-idp-v1alpha1-githuballowedauthorganizationspolicy[$$GitHubAllowedAuthOrganizationsPolicy$$]__ | Policy must be set to "AllGitHubUsers" if allowed is empty. + + + +This field only exists to ensure that Pinniped administrators are aware that an empty list of + +allowedOrganizations means all GitHub users are allowed to log in. + +| *`allowed`* __string array__ | Allowed, when specified, indicates that only users with membership in at least one of the listed + +GitHub organizations may log in. In addition, the group membership presented to Kubernetes will only include + +teams within the listed GitHub organizations. Additional login rules or group filtering can optionally be + +provided as policy expression on any Pinniped Supervisor FederationDomain that includes this IDP. + + + +The configured GitHub App or GitHub OAuth App must be allowed to see membership in the listed organizations, + +otherwise Pinniped will not be aware that the user belongs to the listed organization or any teams + +within that organization. + + + +If no organizations are listed, you must set organizations: AllGitHubUsers. + +|=== + + +[id="{anchor_prefix}-go-pinniped-dev-generated-1-28-apis-supervisor-idp-v1alpha1-githubusernameattribute"] +==== GitHubUsernameAttribute (string) + +GitHubUsernameAttribute allows the user to specify which attribute(s) from GitHub to use for the username to present +to Kubernetes. See the response schema for +[Get the authenticated user](https://docs.github.com/en/rest/users/users?apiVersion=2022-11-28#get-the-authenticated-user). + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-28-apis-supervisor-idp-v1alpha1-githubclaims[$$GitHubClaims$$] +**** + + + [id="{anchor_prefix}-go-pinniped-dev-generated-1-28-apis-supervisor-idp-v1alpha1-ldapidentityprovider"] ==== LDAPIdentityProvider @@ -2108,11 +2387,12 @@ Parameter is a key/value pair which represents a parameter in an HTTP request. [id="{anchor_prefix}-go-pinniped-dev-generated-1-28-apis-supervisor-idp-v1alpha1-tlsspec"] ==== TLSSpec -Configuration for TLS parameters related to identity provider integration. +TLSSpec provides TLS configuration for identity provider integration. .Appears In: **** - xref:{anchor_prefix}-go-pinniped-dev-generated-1-28-apis-supervisor-idp-v1alpha1-activedirectoryidentityproviderspec[$$ActiveDirectoryIdentityProviderSpec$$] +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-28-apis-supervisor-idp-v1alpha1-githubapiconfig[$$GitHubAPIConfig$$] - xref:{anchor_prefix}-go-pinniped-dev-generated-1-28-apis-supervisor-idp-v1alpha1-ldapidentityproviderspec[$$LDAPIdentityProviderSpec$$] - xref:{anchor_prefix}-go-pinniped-dev-generated-1-28-apis-supervisor-idp-v1alpha1-oidcidentityproviderspec[$$OIDCIdentityProviderSpec$$] **** diff --git a/generated/1.28/apis/go.mod b/generated/1.28/apis/go.mod index 67a146a63..1b7ec5811 100644 --- a/generated/1.28/apis/go.mod +++ b/generated/1.28/apis/go.mod @@ -4,6 +4,6 @@ module go.pinniped.dev/generated/1.28/apis go 1.13 require ( - k8s.io/api v0.28.9 - k8s.io/apimachinery v0.28.9 + k8s.io/api v0.28.10 + k8s.io/apimachinery v0.28.10 ) diff --git a/generated/1.28/apis/go.sum b/generated/1.28/apis/go.sum index e2914d9e5..a854d433c 100644 --- a/generated/1.28/apis/go.sum +++ b/generated/1.28/apis/go.sum @@ -297,10 +297,10 @@ gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -k8s.io/api v0.28.9 h1:E7VEXXCAlSrp+08zq4zgd+ko6Ttu0Mw+XoXlIkDTVW0= -k8s.io/api v0.28.9/go.mod h1:AnCsDYf3SHjfa8mPG5LGYf+iF4mie+3peLQR51MMCgw= -k8s.io/apimachinery v0.28.9 h1:aXz4Zxsw+Pk4KhBerAtKRxNN1uSMWKfciL/iOdBfXvA= -k8s.io/apimachinery v0.28.9/go.mod h1:zUG757HaKs6Dc3iGtKjzIpBfqTM4yiRsEe3/E7NX15o= +k8s.io/api v0.28.10 h1:q1Y+h3F+siuwP/qCQuqgqGJjaIuQWN0yFE7z367E3Q0= +k8s.io/api v0.28.10/go.mod h1:u6EzGdzmEC2vfhyw4sD89i7OIc/2v1EAwvd1t4chQac= +k8s.io/apimachinery v0.28.10 h1:cWonrYsJK3lbuf9IgMs5+L5Jzw6QR3ZGA3hzwG0HDeI= +k8s.io/apimachinery v0.28.10/go.mod h1:zUG757HaKs6Dc3iGtKjzIpBfqTM4yiRsEe3/E7NX15o= k8s.io/gengo v0.0.0-20210813121822-485abfe95c7c/go.mod h1:FiNAH4ZV3gBg2Kwh89tzAEV2be7d5xI0vBa/VySYy3E= k8s.io/klog/v2 v2.0.0/go.mod h1:PBfzABfn139FHAV07az/IF9Wp1bkk3vpT2XSJ76fSDE= k8s.io/klog/v2 v2.2.0/go.mod h1:Od+F08eJP+W3HUb4pSrPpgp9DGU4GzlpG/TmITuYh/Y= diff --git a/generated/1.28/apis/supervisor/idp/v1alpha1/register.go b/generated/1.28/apis/supervisor/idp/v1alpha1/register.go index 8829a8638..705be8076 100644 --- a/generated/1.28/apis/supervisor/idp/v1alpha1/register.go +++ b/generated/1.28/apis/supervisor/idp/v1alpha1/register.go @@ -1,4 +1,4 @@ -// Copyright 2020-2022 the Pinniped contributors. All Rights Reserved. +// Copyright 2020-2024 the Pinniped contributors. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 package v1alpha1 @@ -36,6 +36,8 @@ func addKnownTypes(scheme *runtime.Scheme) error { &LDAPIdentityProviderList{}, &ActiveDirectoryIdentityProvider{}, &ActiveDirectoryIdentityProviderList{}, + &GitHubIdentityProvider{}, + &GitHubIdentityProviderList{}, ) metav1.AddToGroupVersion(scheme, SchemeGroupVersion) return nil diff --git a/generated/1.28/apis/supervisor/idp/v1alpha1/types_githubidentityprovider.go b/generated/1.28/apis/supervisor/idp/v1alpha1/types_githubidentityprovider.go new file mode 100644 index 000000000..c84f46dbd --- /dev/null +++ b/generated/1.28/apis/supervisor/idp/v1alpha1/types_githubidentityprovider.go @@ -0,0 +1,256 @@ +// Copyright 2024 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package v1alpha1 + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +type GitHubIdentityProviderPhase string + +const ( + // GitHubPhasePending is the default phase for newly-created GitHubIdentityProvider resources. + GitHubPhasePending GitHubIdentityProviderPhase = "Pending" + + // GitHubPhaseReady is the phase for an GitHubIdentityProvider resource in a healthy state. + GitHubPhaseReady GitHubIdentityProviderPhase = "Ready" + + // GitHubPhaseError is the phase for an GitHubIdentityProvider in an unhealthy state. + GitHubPhaseError GitHubIdentityProviderPhase = "Error" +) + +type GitHubAllowedAuthOrganizationsPolicy string + +const ( + // GitHubAllowedAuthOrganizationsPolicyAllGitHubUsers means any GitHub user is allowed to log in using this identity + // provider, regardless of their organization membership or lack thereof. + GitHubAllowedAuthOrganizationsPolicyAllGitHubUsers GitHubAllowedAuthOrganizationsPolicy = "AllGitHubUsers" + + // GitHubAllowedAuthOrganizationsPolicyOnlyUsersFromAllowedOrganizations means only those users with membership in + // the listed GitHub organizations are allowed to log in. + GitHubAllowedAuthOrganizationsPolicyOnlyUsersFromAllowedOrganizations GitHubAllowedAuthOrganizationsPolicy = "OnlyUsersFromAllowedOrganizations" +) + +// GitHubIdentityProviderStatus is the status of an GitHub identity provider. +type GitHubIdentityProviderStatus struct { + // Phase summarizes the overall status of the GitHubIdentityProvider. + // + // +kubebuilder:default=Pending + // +kubebuilder:validation:Enum=Pending;Ready;Error + Phase GitHubIdentityProviderPhase `json:"phase,omitempty"` + + // Conditions represents the observations of an identity provider's current state. + // + // +patchMergeKey=type + // +patchStrategy=merge + // +listType=map + // +listMapKey=type + Conditions []metav1.Condition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type"` +} + +// GitHubAPIConfig allows configuration for GitHub Enterprise Server +type GitHubAPIConfig struct { + // Host is required only for GitHub Enterprise Server. + // Defaults to using GitHub's public API ("github.com"). + // Do not specify a protocol or scheme since "https://" will always be used. + // Port is optional. Do not specify a path, query, fragment, or userinfo. + // Only domain name or IP address, subdomains (optional), and port (optional). + // IPv4 and IPv6 are supported. If using an IPv6 address with a port, you must enclose the IPv6 address + // in square brackets. Example: "[::1]:443". + // + // +kubebuilder:default="github.com" + // +kubebuilder:validation:MinLength=1 + // +optional + Host *string `json:"host"` + + // TLS configuration for GitHub Enterprise Server. + // + // +optional + TLS *TLSSpec `json:"tls,omitempty"` +} + +// GitHubUsernameAttribute allows the user to specify which attribute(s) from GitHub to use for the username to present +// to Kubernetes. See the response schema for +// [Get the authenticated user](https://docs.github.com/en/rest/users/users?apiVersion=2022-11-28#get-the-authenticated-user). +type GitHubUsernameAttribute string + +const ( + // GitHubUsernameID specifies using the `id` attribute from the GitHub user for the username to present to Kubernetes. + GitHubUsernameID GitHubUsernameAttribute = "id" + + // GitHubUsernameLogin specifies using the `login` attribute from the GitHub user as the username to present to Kubernetes. + GitHubUsernameLogin GitHubUsernameAttribute = "login" + + // GitHubUsernameLoginAndID specifies combining the `login` and `id` attributes from the GitHub user as the + // username to present to Kubernetes, separated by a colon. Example: "my-login:1234" + GitHubUsernameLoginAndID GitHubUsernameAttribute = "login:id" +) + +// GitHubGroupNameAttribute allows the user to specify which attribute from GitHub to use for the group +// names to present to Kubernetes. See the response schema for +// [List teams for the authenticated user](https://docs.github.com/en/rest/teams/teams?apiVersion=2022-11-28#list-teams-for-the-authenticated-user). +type GitHubGroupNameAttribute string + +const ( + // GitHubUseTeamNameForGroupName specifies using the GitHub team's `name` attribute as the group name to present to Kubernetes. + GitHubUseTeamNameForGroupName GitHubGroupNameAttribute = "name" + + // GitHubUseTeamSlugForGroupName specifies using the GitHub team's `slug` attribute as the group name to present to Kubernetes. + GitHubUseTeamSlugForGroupName GitHubGroupNameAttribute = "slug" +) + +// GitHubClaims allows customization of the username and groups claims. +type GitHubClaims struct { + // Username configures which property of the GitHub user record shall determine the username in Kubernetes. + // + // Can be either "id", "login", or "login:id". Defaults to "login:id". + // + // GitHub's user login attributes can only contain alphanumeric characters and non-repeating hyphens, + // and may not start or end with hyphens. GitHub users are allowed to change their login name, + // although it is inconvenient. If a GitHub user changed their login name from "foo" to "bar", + // then a second user might change their name from "baz" to "foo" in order to take the old + // username of the first user. For this reason, it is not as safe to make authorization decisions + // based only on the user's login attribute. + // + // If desired, an admin could configure identity transformation expressions on the Pinniped Supervisor's + // FederationDomain to further customize how these usernames are presented to Kubernetes. + // + // Defaults to "login:id", which is the user login attribute, followed by a colon, followed by the unique and + // unchanging integer ID number attribute. This blends human-readable login names with the unchanging ID value + // from GitHub. Colons are not allowed in GitHub login attributes or ID numbers, so this is a reasonable + // choice to concatenate the two values. + // + // See the response schema for + // [Get the authenticated user](https://docs.github.com/en/rest/users/users?apiVersion=2022-11-28#get-the-authenticated-user). + // + // +kubebuilder:default="login:id" + // +kubebuilder:validation:Enum={"id","login","login:id"} + // +optional + Username *GitHubUsernameAttribute `json:"username"` + + // Groups configures which property of the GitHub team record shall determine the group names in Kubernetes. + // + // Can be either "name" or "slug". Defaults to "slug". + // + // GitHub team names can contain upper and lower case characters, whitespace, and punctuation (e.g. "Kube admins!"). + // + // GitHub team slugs are lower case alphanumeric characters and may contain dashes and underscores (e.g. "kube-admins"). + // + // Group names as presented to Kubernetes will always be prefixed by the GitHub organization name followed by a + // forward slash (e.g. "my-org/my-team"). GitHub organization login names can only contain alphanumeric characters + // or single hyphens, so the first forward slash `/` will be the separator between the organization login name and + // the team name or slug. + // + // If desired, an admin could configure identity transformation expressions on the Pinniped Supervisor's + // FederationDomain to further customize how these group names are presented to Kubernetes. + // + // See the response schema for + // [List teams for the authenticated user](https://docs.github.com/en/rest/teams/teams?apiVersion=2022-11-28#list-teams-for-the-authenticated-user). + // + // +kubebuilder:default=slug + // +kubebuilder:validation:Enum=name;slug + // +optional + Groups *GitHubGroupNameAttribute `json:"groups"` +} + +// GitHubClientSpec contains information about the GitHub client that this identity provider will use +// for web-based login flows. +type GitHubClientSpec struct { + // SecretName contains the name of a namespace-local Secret object that provides the clientID and + // clientSecret for an GitHub App or GitHub OAuth2 client. + // + // This secret must be of type "secrets.pinniped.dev/github-client" with keys "clientID" and "clientSecret". + // + // +kubebuilder:validation:MinLength=1 + SecretName string `json:"secretName"` +} + +type GitHubOrganizationsSpec struct { + // Policy must be set to "AllGitHubUsers" if allowed is empty. + // + // This field only exists to ensure that Pinniped administrators are aware that an empty list of + // allowedOrganizations means all GitHub users are allowed to log in. + // + // +kubebuilder:default=OnlyUsersFromAllowedOrganizations + // +kubebuilder:validation:Enum=OnlyUsersFromAllowedOrganizations;AllGitHubUsers + // +optional + Policy *GitHubAllowedAuthOrganizationsPolicy `json:"policy"` + + // Allowed, when specified, indicates that only users with membership in at least one of the listed + // GitHub organizations may log in. In addition, the group membership presented to Kubernetes will only include + // teams within the listed GitHub organizations. Additional login rules or group filtering can optionally be + // provided as policy expression on any Pinniped Supervisor FederationDomain that includes this IDP. + // + // The configured GitHub App or GitHub OAuth App must be allowed to see membership in the listed organizations, + // otherwise Pinniped will not be aware that the user belongs to the listed organization or any teams + // within that organization. + // + // If no organizations are listed, you must set organizations: AllGitHubUsers. + // + // +kubebuilder:validation:MaxItems=64 + // +listType=set + // +optional + Allowed []string `json:"allowed,omitempty"` +} + +// GitHubAllowAuthenticationSpec allows customization of who can authenticate using this IDP and how. +type GitHubAllowAuthenticationSpec struct { + // Organizations allows customization of which organizations can authenticate using this IDP. + // +kubebuilder:validation:XValidation:message="spec.allowAuthentication.organizations.policy must be 'OnlyUsersFromAllowedOrganizations' when spec.allowAuthentication.organizations.allowed has organizations listed",rule="!(has(self.allowed) && size(self.allowed) > 0 && self.policy == 'AllGitHubUsers')" + // +kubebuilder:validation:XValidation:message="spec.allowAuthentication.organizations.policy must be 'AllGitHubUsers' when spec.allowAuthentication.organizations.allowed is empty",rule="!((!has(self.allowed) || size(self.allowed) == 0) && self.policy == 'OnlyUsersFromAllowedOrganizations')" + Organizations GitHubOrganizationsSpec `json:"organizations"` +} + +// GitHubIdentityProviderSpec is the spec for configuring an GitHub identity provider. +type GitHubIdentityProviderSpec struct { + // GitHubAPI allows configuration for GitHub Enterprise Server + // + // +kubebuilder:default={} + GitHubAPI GitHubAPIConfig `json:"githubAPI,omitempty"` + + // Claims allows customization of the username and groups claims. + // + // +kubebuilder:default={} + Claims GitHubClaims `json:"claims,omitempty"` + + // AllowAuthentication allows customization of who can authenticate using this IDP and how. + AllowAuthentication GitHubAllowAuthenticationSpec `json:"allowAuthentication"` + + // Client identifies the secret with credentials for a GitHub App or GitHub OAuth2 App (a GitHub client). + Client GitHubClientSpec `json:"client"` +} + +// GitHubIdentityProvider describes the configuration of an upstream GitHub identity provider. +// This upstream provider can be configured with either a GitHub App or a GitHub OAuth2 App. +// +// Right now, only web-based logins are supported, for both the pinniped-cli client and clients configured +// as OIDCClients. +// +// +genclient +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object +// +kubebuilder:resource:categories=pinniped;pinniped-idp;pinniped-idps +// +kubebuilder:printcolumn:name="Host",type=string,JSONPath=`.spec.githubAPI.host` +// +kubebuilder:printcolumn:name="Status",type=string,JSONPath=`.status.phase` +// +kubebuilder:printcolumn:name="Age",type=date,JSONPath=`.metadata.creationTimestamp` +// +kubebuilder:subresource:status +type GitHubIdentityProvider struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + // Spec for configuring the identity provider. + Spec GitHubIdentityProviderSpec `json:"spec"` + + // Status of the identity provider. + Status GitHubIdentityProviderStatus `json:"status,omitempty"` +} + +// GitHubIdentityProviderList lists GitHubIdentityProvider objects. +// +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object +type GitHubIdentityProviderList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + + Items []GitHubIdentityProvider `json:"items"` +} diff --git a/generated/1.28/apis/supervisor/idp/v1alpha1/types_tls.go b/generated/1.28/apis/supervisor/idp/v1alpha1/types_tls.go index 1413a262c..49b49373c 100644 --- a/generated/1.28/apis/supervisor/idp/v1alpha1/types_tls.go +++ b/generated/1.28/apis/supervisor/idp/v1alpha1/types_tls.go @@ -1,9 +1,9 @@ -// Copyright 2020-2022 the Pinniped contributors. All Rights Reserved. +// Copyright 2020-2024 the Pinniped contributors. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 package v1alpha1 -// Configuration for TLS parameters related to identity provider integration. +// TLSSpec provides TLS configuration for identity provider integration. type TLSSpec struct { // X.509 Certificate Authority (base64-encoded PEM bundle). If omitted, a default set of system roots will be trusted. // +optional diff --git a/generated/1.28/apis/supervisor/idp/v1alpha1/zz_generated.deepcopy.go b/generated/1.28/apis/supervisor/idp/v1alpha1/zz_generated.deepcopy.go index b0cb168b5..e48860e82 100644 --- a/generated/1.28/apis/supervisor/idp/v1alpha1/zz_generated.deepcopy.go +++ b/generated/1.28/apis/supervisor/idp/v1alpha1/zz_generated.deepcopy.go @@ -203,6 +203,221 @@ func (in *ActiveDirectoryIdentityProviderUserSearchAttributes) DeepCopy() *Activ return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *GitHubAPIConfig) DeepCopyInto(out *GitHubAPIConfig) { + *out = *in + if in.Host != nil { + in, out := &in.Host, &out.Host + *out = new(string) + **out = **in + } + if in.TLS != nil { + in, out := &in.TLS, &out.TLS + *out = new(TLSSpec) + **out = **in + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GitHubAPIConfig. +func (in *GitHubAPIConfig) DeepCopy() *GitHubAPIConfig { + if in == nil { + return nil + } + out := new(GitHubAPIConfig) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *GitHubAllowAuthenticationSpec) DeepCopyInto(out *GitHubAllowAuthenticationSpec) { + *out = *in + in.Organizations.DeepCopyInto(&out.Organizations) + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GitHubAllowAuthenticationSpec. +func (in *GitHubAllowAuthenticationSpec) DeepCopy() *GitHubAllowAuthenticationSpec { + if in == nil { + return nil + } + out := new(GitHubAllowAuthenticationSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *GitHubClaims) DeepCopyInto(out *GitHubClaims) { + *out = *in + if in.Username != nil { + in, out := &in.Username, &out.Username + *out = new(GitHubUsernameAttribute) + **out = **in + } + if in.Groups != nil { + in, out := &in.Groups, &out.Groups + *out = new(GitHubGroupNameAttribute) + **out = **in + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GitHubClaims. +func (in *GitHubClaims) DeepCopy() *GitHubClaims { + if in == nil { + return nil + } + out := new(GitHubClaims) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *GitHubClientSpec) DeepCopyInto(out *GitHubClientSpec) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GitHubClientSpec. +func (in *GitHubClientSpec) DeepCopy() *GitHubClientSpec { + if in == nil { + return nil + } + out := new(GitHubClientSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *GitHubIdentityProvider) DeepCopyInto(out *GitHubIdentityProvider) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + in.Status.DeepCopyInto(&out.Status) + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GitHubIdentityProvider. +func (in *GitHubIdentityProvider) DeepCopy() *GitHubIdentityProvider { + if in == nil { + return nil + } + out := new(GitHubIdentityProvider) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *GitHubIdentityProvider) 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 *GitHubIdentityProviderList) DeepCopyInto(out *GitHubIdentityProviderList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]GitHubIdentityProvider, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GitHubIdentityProviderList. +func (in *GitHubIdentityProviderList) DeepCopy() *GitHubIdentityProviderList { + if in == nil { + return nil + } + out := new(GitHubIdentityProviderList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *GitHubIdentityProviderList) 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 *GitHubIdentityProviderSpec) DeepCopyInto(out *GitHubIdentityProviderSpec) { + *out = *in + in.GitHubAPI.DeepCopyInto(&out.GitHubAPI) + in.Claims.DeepCopyInto(&out.Claims) + in.AllowAuthentication.DeepCopyInto(&out.AllowAuthentication) + out.Client = in.Client + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GitHubIdentityProviderSpec. +func (in *GitHubIdentityProviderSpec) DeepCopy() *GitHubIdentityProviderSpec { + if in == nil { + return nil + } + out := new(GitHubIdentityProviderSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *GitHubIdentityProviderStatus) DeepCopyInto(out *GitHubIdentityProviderStatus) { + *out = *in + if in.Conditions != nil { + in, out := &in.Conditions, &out.Conditions + *out = make([]v1.Condition, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GitHubIdentityProviderStatus. +func (in *GitHubIdentityProviderStatus) DeepCopy() *GitHubIdentityProviderStatus { + if in == nil { + return nil + } + out := new(GitHubIdentityProviderStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *GitHubOrganizationsSpec) DeepCopyInto(out *GitHubOrganizationsSpec) { + *out = *in + if in.Policy != nil { + in, out := &in.Policy, &out.Policy + *out = new(GitHubAllowedAuthOrganizationsPolicy) + **out = **in + } + if in.Allowed != nil { + in, out := &in.Allowed, &out.Allowed + *out = make([]string, len(*in)) + copy(*out, *in) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GitHubOrganizationsSpec. +func (in *GitHubOrganizationsSpec) DeepCopy() *GitHubOrganizationsSpec { + if in == nil { + return nil + } + out := new(GitHubOrganizationsSpec) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *LDAPIdentityProvider) DeepCopyInto(out *LDAPIdentityProvider) { *out = *in diff --git a/generated/1.28/apis/supervisor/idpdiscovery/v1alpha1/types_supervisor_idp_discovery.go b/generated/1.28/apis/supervisor/idpdiscovery/v1alpha1/types_supervisor_idp_discovery.go index 837fed2dd..11be6f9dc 100644 --- a/generated/1.28/apis/supervisor/idpdiscovery/v1alpha1/types_supervisor_idp_discovery.go +++ b/generated/1.28/apis/supervisor/idpdiscovery/v1alpha1/types_supervisor_idp_discovery.go @@ -15,6 +15,7 @@ const ( IDPTypeOIDC IDPType = "oidc" IDPTypeLDAP IDPType = "ldap" IDPTypeActiveDirectory IDPType = "activedirectory" + IDPTypeGitHub IDPType = "github" IDPFlowCLIPassword IDPFlow = "cli_password" IDPFlowBrowserAuthcode IDPFlow = "browser_authcode" diff --git a/generated/1.28/client/go.mod b/generated/1.28/client/go.mod index 167f036a3..3d00d4481 100644 --- a/generated/1.28/client/go.mod +++ b/generated/1.28/client/go.mod @@ -7,7 +7,7 @@ replace go.pinniped.dev/generated/1.28/apis => ../apis require ( go.pinniped.dev/generated/1.28/apis v0.0.0 - k8s.io/apimachinery v0.28.9 - k8s.io/client-go v0.28.9 + k8s.io/apimachinery v0.28.10 + k8s.io/client-go v0.28.10 k8s.io/kube-openapi v0.0.0-20230717233707-2695361300d9 ) diff --git a/generated/1.28/client/go.sum b/generated/1.28/client/go.sum index 1f5edadbd..368e101c1 100644 --- a/generated/1.28/client/go.sum +++ b/generated/1.28/client/go.sum @@ -334,12 +334,12 @@ gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -k8s.io/api v0.28.9 h1:E7VEXXCAlSrp+08zq4zgd+ko6Ttu0Mw+XoXlIkDTVW0= -k8s.io/api v0.28.9/go.mod h1:AnCsDYf3SHjfa8mPG5LGYf+iF4mie+3peLQR51MMCgw= -k8s.io/apimachinery v0.28.9 h1:aXz4Zxsw+Pk4KhBerAtKRxNN1uSMWKfciL/iOdBfXvA= -k8s.io/apimachinery v0.28.9/go.mod h1:zUG757HaKs6Dc3iGtKjzIpBfqTM4yiRsEe3/E7NX15o= -k8s.io/client-go v0.28.9 h1:mmMvejwc/KDjMLmDpyaxkWNzlWRCJ6ht7Qsbsnwn39Y= -k8s.io/client-go v0.28.9/go.mod h1:GFDy3rUNId++WGrr0hRaBrs+y1eZz5JtVZODEalhRMo= +k8s.io/api v0.28.10 h1:q1Y+h3F+siuwP/qCQuqgqGJjaIuQWN0yFE7z367E3Q0= +k8s.io/api v0.28.10/go.mod h1:u6EzGdzmEC2vfhyw4sD89i7OIc/2v1EAwvd1t4chQac= +k8s.io/apimachinery v0.28.10 h1:cWonrYsJK3lbuf9IgMs5+L5Jzw6QR3ZGA3hzwG0HDeI= +k8s.io/apimachinery v0.28.10/go.mod h1:zUG757HaKs6Dc3iGtKjzIpBfqTM4yiRsEe3/E7NX15o= +k8s.io/client-go v0.28.10 h1:y+mvUei3+RU0rE7r2BZFA2ApTAsXSN1glGs4QfULLt4= +k8s.io/client-go v0.28.10/go.mod h1:JLwjCWhQhvm1F4J+7YAr9WVhSRNmfkRofPWU43m8LZk= k8s.io/gengo v0.0.0-20210813121822-485abfe95c7c/go.mod h1:FiNAH4ZV3gBg2Kwh89tzAEV2be7d5xI0vBa/VySYy3E= k8s.io/klog/v2 v2.0.0/go.mod h1:PBfzABfn139FHAV07az/IF9Wp1bkk3vpT2XSJ76fSDE= k8s.io/klog/v2 v2.2.0/go.mod h1:Od+F08eJP+W3HUb4pSrPpgp9DGU4GzlpG/TmITuYh/Y= diff --git a/generated/1.28/client/supervisor/clientset/versioned/typed/idp/v1alpha1/fake/fake_githubidentityprovider.go b/generated/1.28/client/supervisor/clientset/versioned/typed/idp/v1alpha1/fake/fake_githubidentityprovider.go new file mode 100644 index 000000000..fa7edc00e --- /dev/null +++ b/generated/1.28/client/supervisor/clientset/versioned/typed/idp/v1alpha1/fake/fake_githubidentityprovider.go @@ -0,0 +1,128 @@ +// Copyright 2020-2024 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +// Code generated by client-gen. DO NOT EDIT. + +package fake + +import ( + "context" + + v1alpha1 "go.pinniped.dev/generated/1.28/apis/supervisor/idp/v1alpha1" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + labels "k8s.io/apimachinery/pkg/labels" + types "k8s.io/apimachinery/pkg/types" + watch "k8s.io/apimachinery/pkg/watch" + testing "k8s.io/client-go/testing" +) + +// FakeGitHubIdentityProviders implements GitHubIdentityProviderInterface +type FakeGitHubIdentityProviders struct { + Fake *FakeIDPV1alpha1 + ns string +} + +var githubidentityprovidersResource = v1alpha1.SchemeGroupVersion.WithResource("githubidentityproviders") + +var githubidentityprovidersKind = v1alpha1.SchemeGroupVersion.WithKind("GitHubIdentityProvider") + +// Get takes name of the gitHubIdentityProvider, and returns the corresponding gitHubIdentityProvider object, and an error if there is any. +func (c *FakeGitHubIdentityProviders) Get(ctx context.Context, name string, options v1.GetOptions) (result *v1alpha1.GitHubIdentityProvider, err error) { + obj, err := c.Fake. + Invokes(testing.NewGetAction(githubidentityprovidersResource, c.ns, name), &v1alpha1.GitHubIdentityProvider{}) + + if obj == nil { + return nil, err + } + return obj.(*v1alpha1.GitHubIdentityProvider), err +} + +// List takes label and field selectors, and returns the list of GitHubIdentityProviders that match those selectors. +func (c *FakeGitHubIdentityProviders) List(ctx context.Context, opts v1.ListOptions) (result *v1alpha1.GitHubIdentityProviderList, err error) { + obj, err := c.Fake. + Invokes(testing.NewListAction(githubidentityprovidersResource, githubidentityprovidersKind, c.ns, opts), &v1alpha1.GitHubIdentityProviderList{}) + + if obj == nil { + return nil, err + } + + label, _, _ := testing.ExtractFromListOptions(opts) + if label == nil { + label = labels.Everything() + } + list := &v1alpha1.GitHubIdentityProviderList{ListMeta: obj.(*v1alpha1.GitHubIdentityProviderList).ListMeta} + for _, item := range obj.(*v1alpha1.GitHubIdentityProviderList).Items { + if label.Matches(labels.Set(item.Labels)) { + list.Items = append(list.Items, item) + } + } + return list, err +} + +// Watch returns a watch.Interface that watches the requested gitHubIdentityProviders. +func (c *FakeGitHubIdentityProviders) Watch(ctx context.Context, opts v1.ListOptions) (watch.Interface, error) { + return c.Fake. + InvokesWatch(testing.NewWatchAction(githubidentityprovidersResource, c.ns, opts)) + +} + +// Create takes the representation of a gitHubIdentityProvider and creates it. Returns the server's representation of the gitHubIdentityProvider, and an error, if there is any. +func (c *FakeGitHubIdentityProviders) Create(ctx context.Context, gitHubIdentityProvider *v1alpha1.GitHubIdentityProvider, opts v1.CreateOptions) (result *v1alpha1.GitHubIdentityProvider, err error) { + obj, err := c.Fake. + Invokes(testing.NewCreateAction(githubidentityprovidersResource, c.ns, gitHubIdentityProvider), &v1alpha1.GitHubIdentityProvider{}) + + if obj == nil { + return nil, err + } + return obj.(*v1alpha1.GitHubIdentityProvider), err +} + +// Update takes the representation of a gitHubIdentityProvider and updates it. Returns the server's representation of the gitHubIdentityProvider, and an error, if there is any. +func (c *FakeGitHubIdentityProviders) Update(ctx context.Context, gitHubIdentityProvider *v1alpha1.GitHubIdentityProvider, opts v1.UpdateOptions) (result *v1alpha1.GitHubIdentityProvider, err error) { + obj, err := c.Fake. + Invokes(testing.NewUpdateAction(githubidentityprovidersResource, c.ns, gitHubIdentityProvider), &v1alpha1.GitHubIdentityProvider{}) + + if obj == nil { + return nil, err + } + return obj.(*v1alpha1.GitHubIdentityProvider), err +} + +// UpdateStatus was generated because the type contains a Status member. +// Add a +genclient:noStatus comment above the type to avoid generating UpdateStatus(). +func (c *FakeGitHubIdentityProviders) UpdateStatus(ctx context.Context, gitHubIdentityProvider *v1alpha1.GitHubIdentityProvider, opts v1.UpdateOptions) (*v1alpha1.GitHubIdentityProvider, error) { + obj, err := c.Fake. + Invokes(testing.NewUpdateSubresourceAction(githubidentityprovidersResource, "status", c.ns, gitHubIdentityProvider), &v1alpha1.GitHubIdentityProvider{}) + + if obj == nil { + return nil, err + } + return obj.(*v1alpha1.GitHubIdentityProvider), err +} + +// Delete takes name of the gitHubIdentityProvider and deletes it. Returns an error if one occurs. +func (c *FakeGitHubIdentityProviders) Delete(ctx context.Context, name string, opts v1.DeleteOptions) error { + _, err := c.Fake. + Invokes(testing.NewDeleteActionWithOptions(githubidentityprovidersResource, c.ns, name, opts), &v1alpha1.GitHubIdentityProvider{}) + + return err +} + +// DeleteCollection deletes a collection of objects. +func (c *FakeGitHubIdentityProviders) DeleteCollection(ctx context.Context, opts v1.DeleteOptions, listOpts v1.ListOptions) error { + action := testing.NewDeleteCollectionAction(githubidentityprovidersResource, c.ns, listOpts) + + _, err := c.Fake.Invokes(action, &v1alpha1.GitHubIdentityProviderList{}) + return err +} + +// Patch applies the patch and returns the patched gitHubIdentityProvider. +func (c *FakeGitHubIdentityProviders) Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts v1.PatchOptions, subresources ...string) (result *v1alpha1.GitHubIdentityProvider, err error) { + obj, err := c.Fake. + Invokes(testing.NewPatchSubresourceAction(githubidentityprovidersResource, c.ns, name, pt, data, subresources...), &v1alpha1.GitHubIdentityProvider{}) + + if obj == nil { + return nil, err + } + return obj.(*v1alpha1.GitHubIdentityProvider), err +} diff --git a/generated/1.28/client/supervisor/clientset/versioned/typed/idp/v1alpha1/fake/fake_idp_client.go b/generated/1.28/client/supervisor/clientset/versioned/typed/idp/v1alpha1/fake/fake_idp_client.go index ba8d3172b..739569ea5 100644 --- a/generated/1.28/client/supervisor/clientset/versioned/typed/idp/v1alpha1/fake/fake_idp_client.go +++ b/generated/1.28/client/supervisor/clientset/versioned/typed/idp/v1alpha1/fake/fake_idp_client.go @@ -19,6 +19,10 @@ func (c *FakeIDPV1alpha1) ActiveDirectoryIdentityProviders(namespace string) v1a return &FakeActiveDirectoryIdentityProviders{c, namespace} } +func (c *FakeIDPV1alpha1) GitHubIdentityProviders(namespace string) v1alpha1.GitHubIdentityProviderInterface { + return &FakeGitHubIdentityProviders{c, namespace} +} + func (c *FakeIDPV1alpha1) LDAPIdentityProviders(namespace string) v1alpha1.LDAPIdentityProviderInterface { return &FakeLDAPIdentityProviders{c, namespace} } diff --git a/generated/1.28/client/supervisor/clientset/versioned/typed/idp/v1alpha1/generated_expansion.go b/generated/1.28/client/supervisor/clientset/versioned/typed/idp/v1alpha1/generated_expansion.go index 79be098d6..a7acc8120 100644 --- a/generated/1.28/client/supervisor/clientset/versioned/typed/idp/v1alpha1/generated_expansion.go +++ b/generated/1.28/client/supervisor/clientset/versioned/typed/idp/v1alpha1/generated_expansion.go @@ -7,6 +7,8 @@ package v1alpha1 type ActiveDirectoryIdentityProviderExpansion interface{} +type GitHubIdentityProviderExpansion interface{} + type LDAPIdentityProviderExpansion interface{} type OIDCIdentityProviderExpansion interface{} diff --git a/generated/1.28/client/supervisor/clientset/versioned/typed/idp/v1alpha1/githubidentityprovider.go b/generated/1.28/client/supervisor/clientset/versioned/typed/idp/v1alpha1/githubidentityprovider.go new file mode 100644 index 000000000..6fc738ddb --- /dev/null +++ b/generated/1.28/client/supervisor/clientset/versioned/typed/idp/v1alpha1/githubidentityprovider.go @@ -0,0 +1,182 @@ +// Copyright 2020-2024 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +// Code generated by client-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + "context" + "time" + + v1alpha1 "go.pinniped.dev/generated/1.28/apis/supervisor/idp/v1alpha1" + scheme "go.pinniped.dev/generated/1.28/client/supervisor/clientset/versioned/scheme" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + types "k8s.io/apimachinery/pkg/types" + watch "k8s.io/apimachinery/pkg/watch" + rest "k8s.io/client-go/rest" +) + +// GitHubIdentityProvidersGetter has a method to return a GitHubIdentityProviderInterface. +// A group's client should implement this interface. +type GitHubIdentityProvidersGetter interface { + GitHubIdentityProviders(namespace string) GitHubIdentityProviderInterface +} + +// GitHubIdentityProviderInterface has methods to work with GitHubIdentityProvider resources. +type GitHubIdentityProviderInterface interface { + Create(ctx context.Context, gitHubIdentityProvider *v1alpha1.GitHubIdentityProvider, opts v1.CreateOptions) (*v1alpha1.GitHubIdentityProvider, error) + Update(ctx context.Context, gitHubIdentityProvider *v1alpha1.GitHubIdentityProvider, opts v1.UpdateOptions) (*v1alpha1.GitHubIdentityProvider, error) + UpdateStatus(ctx context.Context, gitHubIdentityProvider *v1alpha1.GitHubIdentityProvider, opts v1.UpdateOptions) (*v1alpha1.GitHubIdentityProvider, 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) (*v1alpha1.GitHubIdentityProvider, error) + List(ctx context.Context, opts v1.ListOptions) (*v1alpha1.GitHubIdentityProviderList, 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 *v1alpha1.GitHubIdentityProvider, err error) + GitHubIdentityProviderExpansion +} + +// gitHubIdentityProviders implements GitHubIdentityProviderInterface +type gitHubIdentityProviders struct { + client rest.Interface + ns string +} + +// newGitHubIdentityProviders returns a GitHubIdentityProviders +func newGitHubIdentityProviders(c *IDPV1alpha1Client, namespace string) *gitHubIdentityProviders { + return &gitHubIdentityProviders{ + client: c.RESTClient(), + ns: namespace, + } +} + +// Get takes name of the gitHubIdentityProvider, and returns the corresponding gitHubIdentityProvider object, and an error if there is any. +func (c *gitHubIdentityProviders) Get(ctx context.Context, name string, options v1.GetOptions) (result *v1alpha1.GitHubIdentityProvider, err error) { + result = &v1alpha1.GitHubIdentityProvider{} + err = c.client.Get(). + Namespace(c.ns). + Resource("githubidentityproviders"). + Name(name). + VersionedParams(&options, scheme.ParameterCodec). + Do(ctx). + Into(result) + return +} + +// List takes label and field selectors, and returns the list of GitHubIdentityProviders that match those selectors. +func (c *gitHubIdentityProviders) List(ctx context.Context, opts v1.ListOptions) (result *v1alpha1.GitHubIdentityProviderList, err error) { + var timeout time.Duration + if opts.TimeoutSeconds != nil { + timeout = time.Duration(*opts.TimeoutSeconds) * time.Second + } + result = &v1alpha1.GitHubIdentityProviderList{} + err = c.client.Get(). + Namespace(c.ns). + Resource("githubidentityproviders"). + VersionedParams(&opts, scheme.ParameterCodec). + Timeout(timeout). + Do(ctx). + Into(result) + return +} + +// Watch returns a watch.Interface that watches the requested gitHubIdentityProviders. +func (c *gitHubIdentityProviders) Watch(ctx context.Context, opts v1.ListOptions) (watch.Interface, error) { + var timeout time.Duration + if opts.TimeoutSeconds != nil { + timeout = time.Duration(*opts.TimeoutSeconds) * time.Second + } + opts.Watch = true + return c.client.Get(). + Namespace(c.ns). + Resource("githubidentityproviders"). + VersionedParams(&opts, scheme.ParameterCodec). + Timeout(timeout). + Watch(ctx) +} + +// Create takes the representation of a gitHubIdentityProvider and creates it. Returns the server's representation of the gitHubIdentityProvider, and an error, if there is any. +func (c *gitHubIdentityProviders) Create(ctx context.Context, gitHubIdentityProvider *v1alpha1.GitHubIdentityProvider, opts v1.CreateOptions) (result *v1alpha1.GitHubIdentityProvider, err error) { + result = &v1alpha1.GitHubIdentityProvider{} + err = c.client.Post(). + Namespace(c.ns). + Resource("githubidentityproviders"). + VersionedParams(&opts, scheme.ParameterCodec). + Body(gitHubIdentityProvider). + Do(ctx). + Into(result) + return +} + +// Update takes the representation of a gitHubIdentityProvider and updates it. Returns the server's representation of the gitHubIdentityProvider, and an error, if there is any. +func (c *gitHubIdentityProviders) Update(ctx context.Context, gitHubIdentityProvider *v1alpha1.GitHubIdentityProvider, opts v1.UpdateOptions) (result *v1alpha1.GitHubIdentityProvider, err error) { + result = &v1alpha1.GitHubIdentityProvider{} + err = c.client.Put(). + Namespace(c.ns). + Resource("githubidentityproviders"). + Name(gitHubIdentityProvider.Name). + VersionedParams(&opts, scheme.ParameterCodec). + Body(gitHubIdentityProvider). + Do(ctx). + Into(result) + return +} + +// UpdateStatus was generated because the type contains a Status member. +// Add a +genclient:noStatus comment above the type to avoid generating UpdateStatus(). +func (c *gitHubIdentityProviders) UpdateStatus(ctx context.Context, gitHubIdentityProvider *v1alpha1.GitHubIdentityProvider, opts v1.UpdateOptions) (result *v1alpha1.GitHubIdentityProvider, err error) { + result = &v1alpha1.GitHubIdentityProvider{} + err = c.client.Put(). + Namespace(c.ns). + Resource("githubidentityproviders"). + Name(gitHubIdentityProvider.Name). + SubResource("status"). + VersionedParams(&opts, scheme.ParameterCodec). + Body(gitHubIdentityProvider). + Do(ctx). + Into(result) + return +} + +// Delete takes name of the gitHubIdentityProvider and deletes it. Returns an error if one occurs. +func (c *gitHubIdentityProviders) Delete(ctx context.Context, name string, opts v1.DeleteOptions) error { + return c.client.Delete(). + Namespace(c.ns). + Resource("githubidentityproviders"). + Name(name). + Body(&opts). + Do(ctx). + Error() +} + +// DeleteCollection deletes a collection of objects. +func (c *gitHubIdentityProviders) DeleteCollection(ctx context.Context, opts v1.DeleteOptions, listOpts v1.ListOptions) error { + var timeout time.Duration + if listOpts.TimeoutSeconds != nil { + timeout = time.Duration(*listOpts.TimeoutSeconds) * time.Second + } + return c.client.Delete(). + Namespace(c.ns). + Resource("githubidentityproviders"). + VersionedParams(&listOpts, scheme.ParameterCodec). + Timeout(timeout). + Body(&opts). + Do(ctx). + Error() +} + +// Patch applies the patch and returns the patched gitHubIdentityProvider. +func (c *gitHubIdentityProviders) Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts v1.PatchOptions, subresources ...string) (result *v1alpha1.GitHubIdentityProvider, err error) { + result = &v1alpha1.GitHubIdentityProvider{} + err = c.client.Patch(pt). + Namespace(c.ns). + Resource("githubidentityproviders"). + Name(name). + SubResource(subresources...). + VersionedParams(&opts, scheme.ParameterCodec). + Body(data). + Do(ctx). + Into(result) + return +} diff --git a/generated/1.28/client/supervisor/clientset/versioned/typed/idp/v1alpha1/idp_client.go b/generated/1.28/client/supervisor/clientset/versioned/typed/idp/v1alpha1/idp_client.go index 270c3aa75..88e24b3b7 100644 --- a/generated/1.28/client/supervisor/clientset/versioned/typed/idp/v1alpha1/idp_client.go +++ b/generated/1.28/client/supervisor/clientset/versioned/typed/idp/v1alpha1/idp_client.go @@ -16,6 +16,7 @@ import ( type IDPV1alpha1Interface interface { RESTClient() rest.Interface ActiveDirectoryIdentityProvidersGetter + GitHubIdentityProvidersGetter LDAPIdentityProvidersGetter OIDCIdentityProvidersGetter } @@ -29,6 +30,10 @@ func (c *IDPV1alpha1Client) ActiveDirectoryIdentityProviders(namespace string) A return newActiveDirectoryIdentityProviders(c, namespace) } +func (c *IDPV1alpha1Client) GitHubIdentityProviders(namespace string) GitHubIdentityProviderInterface { + return newGitHubIdentityProviders(c, namespace) +} + func (c *IDPV1alpha1Client) LDAPIdentityProviders(namespace string) LDAPIdentityProviderInterface { return newLDAPIdentityProviders(c, namespace) } diff --git a/generated/1.28/client/supervisor/informers/externalversions/generic.go b/generated/1.28/client/supervisor/informers/externalversions/generic.go index 424728ebf..942d4fcfd 100644 --- a/generated/1.28/client/supervisor/informers/externalversions/generic.go +++ b/generated/1.28/client/supervisor/informers/externalversions/generic.go @@ -49,6 +49,8 @@ func (f *sharedInformerFactory) ForResource(resource schema.GroupVersionResource // Group=idp.supervisor.pinniped.dev, Version=v1alpha1 case idpv1alpha1.SchemeGroupVersion.WithResource("activedirectoryidentityproviders"): return &genericInformer{resource: resource.GroupResource(), informer: f.IDP().V1alpha1().ActiveDirectoryIdentityProviders().Informer()}, nil + case idpv1alpha1.SchemeGroupVersion.WithResource("githubidentityproviders"): + return &genericInformer{resource: resource.GroupResource(), informer: f.IDP().V1alpha1().GitHubIdentityProviders().Informer()}, nil case idpv1alpha1.SchemeGroupVersion.WithResource("ldapidentityproviders"): return &genericInformer{resource: resource.GroupResource(), informer: f.IDP().V1alpha1().LDAPIdentityProviders().Informer()}, nil case idpv1alpha1.SchemeGroupVersion.WithResource("oidcidentityproviders"): diff --git a/generated/1.28/client/supervisor/informers/externalversions/idp/v1alpha1/githubidentityprovider.go b/generated/1.28/client/supervisor/informers/externalversions/idp/v1alpha1/githubidentityprovider.go new file mode 100644 index 000000000..adf752384 --- /dev/null +++ b/generated/1.28/client/supervisor/informers/externalversions/idp/v1alpha1/githubidentityprovider.go @@ -0,0 +1,77 @@ +// Copyright 2020-2024 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +// Code generated by informer-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + "context" + time "time" + + idpv1alpha1 "go.pinniped.dev/generated/1.28/apis/supervisor/idp/v1alpha1" + versioned "go.pinniped.dev/generated/1.28/client/supervisor/clientset/versioned" + internalinterfaces "go.pinniped.dev/generated/1.28/client/supervisor/informers/externalversions/internalinterfaces" + v1alpha1 "go.pinniped.dev/generated/1.28/client/supervisor/listers/idp/v1alpha1" + 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" +) + +// GitHubIdentityProviderInformer provides access to a shared informer and lister for +// GitHubIdentityProviders. +type GitHubIdentityProviderInformer interface { + Informer() cache.SharedIndexInformer + Lister() v1alpha1.GitHubIdentityProviderLister +} + +type gitHubIdentityProviderInformer struct { + factory internalinterfaces.SharedInformerFactory + tweakListOptions internalinterfaces.TweakListOptionsFunc + namespace string +} + +// NewGitHubIdentityProviderInformer constructs a new informer for GitHubIdentityProvider 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 NewGitHubIdentityProviderInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers) cache.SharedIndexInformer { + return NewFilteredGitHubIdentityProviderInformer(client, namespace, resyncPeriod, indexers, nil) +} + +// NewFilteredGitHubIdentityProviderInformer constructs a new informer for GitHubIdentityProvider 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 NewFilteredGitHubIdentityProviderInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers, tweakListOptions internalinterfaces.TweakListOptionsFunc) cache.SharedIndexInformer { + return cache.NewSharedIndexInformer( + &cache.ListWatch{ + ListFunc: func(options v1.ListOptions) (runtime.Object, error) { + if tweakListOptions != nil { + tweakListOptions(&options) + } + return client.IDPV1alpha1().GitHubIdentityProviders(namespace).List(context.TODO(), options) + }, + WatchFunc: func(options v1.ListOptions) (watch.Interface, error) { + if tweakListOptions != nil { + tweakListOptions(&options) + } + return client.IDPV1alpha1().GitHubIdentityProviders(namespace).Watch(context.TODO(), options) + }, + }, + &idpv1alpha1.GitHubIdentityProvider{}, + resyncPeriod, + indexers, + ) +} + +func (f *gitHubIdentityProviderInformer) defaultInformer(client versioned.Interface, resyncPeriod time.Duration) cache.SharedIndexInformer { + return NewFilteredGitHubIdentityProviderInformer(client, f.namespace, resyncPeriod, cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc}, f.tweakListOptions) +} + +func (f *gitHubIdentityProviderInformer) Informer() cache.SharedIndexInformer { + return f.factory.InformerFor(&idpv1alpha1.GitHubIdentityProvider{}, f.defaultInformer) +} + +func (f *gitHubIdentityProviderInformer) Lister() v1alpha1.GitHubIdentityProviderLister { + return v1alpha1.NewGitHubIdentityProviderLister(f.Informer().GetIndexer()) +} diff --git a/generated/1.28/client/supervisor/informers/externalversions/idp/v1alpha1/interface.go b/generated/1.28/client/supervisor/informers/externalversions/idp/v1alpha1/interface.go index a46748908..b3649bb66 100644 --- a/generated/1.28/client/supervisor/informers/externalversions/idp/v1alpha1/interface.go +++ b/generated/1.28/client/supervisor/informers/externalversions/idp/v1alpha1/interface.go @@ -13,6 +13,8 @@ import ( type Interface interface { // ActiveDirectoryIdentityProviders returns a ActiveDirectoryIdentityProviderInformer. ActiveDirectoryIdentityProviders() ActiveDirectoryIdentityProviderInformer + // GitHubIdentityProviders returns a GitHubIdentityProviderInformer. + GitHubIdentityProviders() GitHubIdentityProviderInformer // LDAPIdentityProviders returns a LDAPIdentityProviderInformer. LDAPIdentityProviders() LDAPIdentityProviderInformer // OIDCIdentityProviders returns a OIDCIdentityProviderInformer. @@ -35,6 +37,11 @@ func (v *version) ActiveDirectoryIdentityProviders() ActiveDirectoryIdentityProv return &activeDirectoryIdentityProviderInformer{factory: v.factory, namespace: v.namespace, tweakListOptions: v.tweakListOptions} } +// GitHubIdentityProviders returns a GitHubIdentityProviderInformer. +func (v *version) GitHubIdentityProviders() GitHubIdentityProviderInformer { + return &gitHubIdentityProviderInformer{factory: v.factory, namespace: v.namespace, tweakListOptions: v.tweakListOptions} +} + // LDAPIdentityProviders returns a LDAPIdentityProviderInformer. func (v *version) LDAPIdentityProviders() LDAPIdentityProviderInformer { return &lDAPIdentityProviderInformer{factory: v.factory, namespace: v.namespace, tweakListOptions: v.tweakListOptions} diff --git a/generated/1.28/client/supervisor/listers/idp/v1alpha1/expansion_generated.go b/generated/1.28/client/supervisor/listers/idp/v1alpha1/expansion_generated.go index b491a9e8d..470c06abb 100644 --- a/generated/1.28/client/supervisor/listers/idp/v1alpha1/expansion_generated.go +++ b/generated/1.28/client/supervisor/listers/idp/v1alpha1/expansion_generated.go @@ -13,6 +13,14 @@ type ActiveDirectoryIdentityProviderListerExpansion interface{} // ActiveDirectoryIdentityProviderNamespaceLister. type ActiveDirectoryIdentityProviderNamespaceListerExpansion interface{} +// GitHubIdentityProviderListerExpansion allows custom methods to be added to +// GitHubIdentityProviderLister. +type GitHubIdentityProviderListerExpansion interface{} + +// GitHubIdentityProviderNamespaceListerExpansion allows custom methods to be added to +// GitHubIdentityProviderNamespaceLister. +type GitHubIdentityProviderNamespaceListerExpansion interface{} + // LDAPIdentityProviderListerExpansion allows custom methods to be added to // LDAPIdentityProviderLister. type LDAPIdentityProviderListerExpansion interface{} diff --git a/generated/1.28/client/supervisor/listers/idp/v1alpha1/githubidentityprovider.go b/generated/1.28/client/supervisor/listers/idp/v1alpha1/githubidentityprovider.go new file mode 100644 index 000000000..fafffc48b --- /dev/null +++ b/generated/1.28/client/supervisor/listers/idp/v1alpha1/githubidentityprovider.go @@ -0,0 +1,86 @@ +// Copyright 2020-2024 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +// Code generated by lister-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + v1alpha1 "go.pinniped.dev/generated/1.28/apis/supervisor/idp/v1alpha1" + "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/client-go/tools/cache" +) + +// GitHubIdentityProviderLister helps list GitHubIdentityProviders. +// All objects returned here must be treated as read-only. +type GitHubIdentityProviderLister interface { + // List lists all GitHubIdentityProviders in the indexer. + // Objects returned here must be treated as read-only. + List(selector labels.Selector) (ret []*v1alpha1.GitHubIdentityProvider, err error) + // GitHubIdentityProviders returns an object that can list and get GitHubIdentityProviders. + GitHubIdentityProviders(namespace string) GitHubIdentityProviderNamespaceLister + GitHubIdentityProviderListerExpansion +} + +// gitHubIdentityProviderLister implements the GitHubIdentityProviderLister interface. +type gitHubIdentityProviderLister struct { + indexer cache.Indexer +} + +// NewGitHubIdentityProviderLister returns a new GitHubIdentityProviderLister. +func NewGitHubIdentityProviderLister(indexer cache.Indexer) GitHubIdentityProviderLister { + return &gitHubIdentityProviderLister{indexer: indexer} +} + +// List lists all GitHubIdentityProviders in the indexer. +func (s *gitHubIdentityProviderLister) List(selector labels.Selector) (ret []*v1alpha1.GitHubIdentityProvider, err error) { + err = cache.ListAll(s.indexer, selector, func(m interface{}) { + ret = append(ret, m.(*v1alpha1.GitHubIdentityProvider)) + }) + return ret, err +} + +// GitHubIdentityProviders returns an object that can list and get GitHubIdentityProviders. +func (s *gitHubIdentityProviderLister) GitHubIdentityProviders(namespace string) GitHubIdentityProviderNamespaceLister { + return gitHubIdentityProviderNamespaceLister{indexer: s.indexer, namespace: namespace} +} + +// GitHubIdentityProviderNamespaceLister helps list and get GitHubIdentityProviders. +// All objects returned here must be treated as read-only. +type GitHubIdentityProviderNamespaceLister interface { + // List lists all GitHubIdentityProviders in the indexer for a given namespace. + // Objects returned here must be treated as read-only. + List(selector labels.Selector) (ret []*v1alpha1.GitHubIdentityProvider, err error) + // Get retrieves the GitHubIdentityProvider from the indexer for a given namespace and name. + // Objects returned here must be treated as read-only. + Get(name string) (*v1alpha1.GitHubIdentityProvider, error) + GitHubIdentityProviderNamespaceListerExpansion +} + +// gitHubIdentityProviderNamespaceLister implements the GitHubIdentityProviderNamespaceLister +// interface. +type gitHubIdentityProviderNamespaceLister struct { + indexer cache.Indexer + namespace string +} + +// List lists all GitHubIdentityProviders in the indexer for a given namespace. +func (s gitHubIdentityProviderNamespaceLister) List(selector labels.Selector) (ret []*v1alpha1.GitHubIdentityProvider, err error) { + err = cache.ListAllByNamespace(s.indexer, s.namespace, selector, func(m interface{}) { + ret = append(ret, m.(*v1alpha1.GitHubIdentityProvider)) + }) + return ret, err +} + +// Get retrieves the GitHubIdentityProvider from the indexer for a given namespace and name. +func (s gitHubIdentityProviderNamespaceLister) Get(name string) (*v1alpha1.GitHubIdentityProvider, error) { + obj, exists, err := s.indexer.GetByKey(s.namespace + "/" + name) + if err != nil { + return nil, err + } + if !exists { + return nil, errors.NewNotFound(v1alpha1.Resource("githubidentityprovider"), name) + } + return obj.(*v1alpha1.GitHubIdentityProvider), nil +} diff --git a/generated/1.28/crds/authentication.concierge.pinniped.dev_jwtauthenticators.yaml b/generated/1.28/crds/authentication.concierge.pinniped.dev_jwtauthenticators.yaml index b464cfa2b..d59fcb783 100644 --- a/generated/1.28/crds/authentication.concierge.pinniped.dev_jwtauthenticators.yaml +++ b/generated/1.28/crds/authentication.concierge.pinniped.dev_jwtauthenticators.yaml @@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - controller-gen.kubebuilder.io/version: v0.14.0 + controller-gen.kubebuilder.io/version: v0.15.0 name: jwtauthenticators.authentication.concierge.pinniped.dev spec: group: authentication.concierge.pinniped.dev diff --git a/generated/1.28/crds/authentication.concierge.pinniped.dev_webhookauthenticators.yaml b/generated/1.28/crds/authentication.concierge.pinniped.dev_webhookauthenticators.yaml index e8cbc6790..4ccd53770 100644 --- a/generated/1.28/crds/authentication.concierge.pinniped.dev_webhookauthenticators.yaml +++ b/generated/1.28/crds/authentication.concierge.pinniped.dev_webhookauthenticators.yaml @@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - controller-gen.kubebuilder.io/version: v0.14.0 + controller-gen.kubebuilder.io/version: v0.15.0 name: webhookauthenticators.authentication.concierge.pinniped.dev spec: group: authentication.concierge.pinniped.dev diff --git a/generated/1.28/crds/config.concierge.pinniped.dev_credentialissuers.yaml b/generated/1.28/crds/config.concierge.pinniped.dev_credentialissuers.yaml index 8d77a6665..26fed7376 100644 --- a/generated/1.28/crds/config.concierge.pinniped.dev_credentialissuers.yaml +++ b/generated/1.28/crds/config.concierge.pinniped.dev_credentialissuers.yaml @@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - controller-gen.kubebuilder.io/version: v0.14.0 + controller-gen.kubebuilder.io/version: v0.15.0 name: credentialissuers.config.concierge.pinniped.dev spec: group: config.concierge.pinniped.dev diff --git a/generated/1.28/crds/config.supervisor.pinniped.dev_federationdomains.yaml b/generated/1.28/crds/config.supervisor.pinniped.dev_federationdomains.yaml index 7bf231443..d43c7406d 100644 --- a/generated/1.28/crds/config.supervisor.pinniped.dev_federationdomains.yaml +++ b/generated/1.28/crds/config.supervisor.pinniped.dev_federationdomains.yaml @@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - controller-gen.kubebuilder.io/version: v0.14.0 + controller-gen.kubebuilder.io/version: v0.15.0 name: federationdomains.config.supervisor.pinniped.dev spec: group: config.supervisor.pinniped.dev diff --git a/generated/1.28/crds/config.supervisor.pinniped.dev_oidcclients.yaml b/generated/1.28/crds/config.supervisor.pinniped.dev_oidcclients.yaml index d33b31bf1..c44ecbf1e 100644 --- a/generated/1.28/crds/config.supervisor.pinniped.dev_oidcclients.yaml +++ b/generated/1.28/crds/config.supervisor.pinniped.dev_oidcclients.yaml @@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - controller-gen.kubebuilder.io/version: v0.14.0 + controller-gen.kubebuilder.io/version: v0.15.0 name: oidcclients.config.supervisor.pinniped.dev spec: group: config.supervisor.pinniped.dev diff --git a/generated/1.28/crds/idp.supervisor.pinniped.dev_activedirectoryidentityproviders.yaml b/generated/1.28/crds/idp.supervisor.pinniped.dev_activedirectoryidentityproviders.yaml index 414ac4f51..062251102 100644 --- a/generated/1.28/crds/idp.supervisor.pinniped.dev_activedirectoryidentityproviders.yaml +++ b/generated/1.28/crds/idp.supervisor.pinniped.dev_activedirectoryidentityproviders.yaml @@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - controller-gen.kubebuilder.io/version: v0.14.0 + controller-gen.kubebuilder.io/version: v0.15.0 name: activedirectoryidentityproviders.idp.supervisor.pinniped.dev spec: group: idp.supervisor.pinniped.dev diff --git a/generated/1.28/crds/idp.supervisor.pinniped.dev_githubidentityproviders.yaml b/generated/1.28/crds/idp.supervisor.pinniped.dev_githubidentityproviders.yaml new file mode 100644 index 000000000..f93108700 --- /dev/null +++ b/generated/1.28/crds/idp.supervisor.pinniped.dev_githubidentityproviders.yaml @@ -0,0 +1,326 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.15.0 + name: githubidentityproviders.idp.supervisor.pinniped.dev +spec: + group: idp.supervisor.pinniped.dev + names: + categories: + - pinniped + - pinniped-idp + - pinniped-idps + kind: GitHubIdentityProvider + listKind: GitHubIdentityProviderList + plural: githubidentityproviders + singular: githubidentityprovider + scope: Namespaced + versions: + - additionalPrinterColumns: + - jsonPath: .spec.githubAPI.host + name: Host + type: string + - jsonPath: .status.phase + name: Status + type: string + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1alpha1 + schema: + openAPIV3Schema: + description: |- + GitHubIdentityProvider describes the configuration of an upstream GitHub identity provider. + This upstream provider can be configured with either a GitHub App or a GitHub OAuth2 App. + + + Right now, only web-based logins are supported, for both the pinniped-cli client and clients configured + as OIDCClients. + 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: + description: Spec for configuring the identity provider. + properties: + allowAuthentication: + description: AllowAuthentication allows customization of who can authenticate + using this IDP and how. + properties: + organizations: + description: Organizations allows customization of which organizations + can authenticate using this IDP. + properties: + allowed: + description: |- + Allowed, when specified, indicates that only users with membership in at least one of the listed + GitHub organizations may log in. In addition, the group membership presented to Kubernetes will only include + teams within the listed GitHub organizations. Additional login rules or group filtering can optionally be + provided as policy expression on any Pinniped Supervisor FederationDomain that includes this IDP. + + + The configured GitHub App or GitHub OAuth App must be allowed to see membership in the listed organizations, + otherwise Pinniped will not be aware that the user belongs to the listed organization or any teams + within that organization. + + + If no organizations are listed, you must set organizations: AllGitHubUsers. + items: + type: string + maxItems: 64 + type: array + x-kubernetes-list-type: set + policy: + default: OnlyUsersFromAllowedOrganizations + description: |- + Policy must be set to "AllGitHubUsers" if allowed is empty. + + + This field only exists to ensure that Pinniped administrators are aware that an empty list of + allowedOrganizations means all GitHub users are allowed to log in. + enum: + - OnlyUsersFromAllowedOrganizations + - AllGitHubUsers + type: string + type: object + x-kubernetes-validations: + - message: spec.allowAuthentication.organizations.policy must + be 'OnlyUsersFromAllowedOrganizations' when spec.allowAuthentication.organizations.allowed + has organizations listed + rule: '!(has(self.allowed) && size(self.allowed) > 0 && self.policy + == ''AllGitHubUsers'')' + - message: spec.allowAuthentication.organizations.policy must + be 'AllGitHubUsers' when spec.allowAuthentication.organizations.allowed + is empty + rule: '!((!has(self.allowed) || size(self.allowed) == 0) && + self.policy == ''OnlyUsersFromAllowedOrganizations'')' + required: + - organizations + type: object + claims: + default: {} + description: Claims allows customization of the username and groups + claims. + properties: + groups: + default: slug + description: |- + Groups configures which property of the GitHub team record shall determine the group names in Kubernetes. + + + Can be either "name" or "slug". Defaults to "slug". + + + GitHub team names can contain upper and lower case characters, whitespace, and punctuation (e.g. "Kube admins!"). + + + GitHub team slugs are lower case alphanumeric characters and may contain dashes and underscores (e.g. "kube-admins"). + + + Group names as presented to Kubernetes will always be prefixed by the GitHub organization name followed by a + forward slash (e.g. "my-org/my-team"). GitHub organization login names can only contain alphanumeric characters + or single hyphens, so the first forward slash `/` will be the separator between the organization login name and + the team name or slug. + + + If desired, an admin could configure identity transformation expressions on the Pinniped Supervisor's + FederationDomain to further customize how these group names are presented to Kubernetes. + + + See the response schema for + [List teams for the authenticated user](https://docs.github.com/en/rest/teams/teams?apiVersion=2022-11-28#list-teams-for-the-authenticated-user). + enum: + - name + - slug + type: string + username: + default: login:id + description: |- + Username configures which property of the GitHub user record shall determine the username in Kubernetes. + + + Can be either "id", "login", or "login:id". Defaults to "login:id". + + + GitHub's user login attributes can only contain alphanumeric characters and non-repeating hyphens, + and may not start or end with hyphens. GitHub users are allowed to change their login name, + although it is inconvenient. If a GitHub user changed their login name from "foo" to "bar", + then a second user might change their name from "baz" to "foo" in order to take the old + username of the first user. For this reason, it is not as safe to make authorization decisions + based only on the user's login attribute. + + + If desired, an admin could configure identity transformation expressions on the Pinniped Supervisor's + FederationDomain to further customize how these usernames are presented to Kubernetes. + + + Defaults to "login:id", which is the user login attribute, followed by a colon, followed by the unique and + unchanging integer ID number attribute. This blends human-readable login names with the unchanging ID value + from GitHub. Colons are not allowed in GitHub login attributes or ID numbers, so this is a reasonable + choice to concatenate the two values. + + + See the response schema for + [Get the authenticated user](https://docs.github.com/en/rest/users/users?apiVersion=2022-11-28#get-the-authenticated-user). + enum: + - id + - login + - login:id + type: string + type: object + client: + description: Client identifies the secret with credentials for a GitHub + App or GitHub OAuth2 App (a GitHub client). + properties: + secretName: + description: |- + SecretName contains the name of a namespace-local Secret object that provides the clientID and + clientSecret for an GitHub App or GitHub OAuth2 client. + + + This secret must be of type "secrets.pinniped.dev/github-client" with keys "clientID" and "clientSecret". + minLength: 1 + type: string + required: + - secretName + type: object + githubAPI: + default: {} + description: GitHubAPI allows configuration for GitHub Enterprise + Server + properties: + host: + default: github.com + description: |- + Host is required only for GitHub Enterprise Server. + Defaults to using GitHub's public API ("github.com"). + Do not specify a protocol or scheme since "https://" will always be used. + Port is optional. Do not specify a path, query, fragment, or userinfo. + Only domain name or IP address, subdomains (optional), and port (optional). + IPv4 and IPv6 are supported. If using an IPv6 address with a port, you must enclose the IPv6 address + in square brackets. Example: "[::1]:443". + minLength: 1 + type: string + tls: + description: TLS configuration for GitHub Enterprise Server. + properties: + certificateAuthorityData: + description: X.509 Certificate Authority (base64-encoded PEM + bundle). If omitted, a default set of system roots will + be trusted. + type: string + type: object + type: object + required: + - allowAuthentication + - client + type: object + status: + description: Status of the identity provider. + properties: + conditions: + description: Conditions represents the observations of an identity + provider's current state. + items: + description: "Condition contains details for one aspect of the current + state of this API Resource.\n---\nThis struct is intended for + direct use as an array at the field path .status.conditions. For + example,\n\n\n\ttype FooStatus struct{\n\t // Represents the + observations of a foo's current state.\n\t // Known .status.conditions.type + are: \"Available\", \"Progressing\", and \"Degraded\"\n\t // + +patchMergeKey=type\n\t // +patchStrategy=merge\n\t // +listType=map\n\t + \ // +listMapKey=type\n\t Conditions []metav1.Condition `json:\"conditions,omitempty\" + patchStrategy:\"merge\" patchMergeKey:\"type\" protobuf:\"bytes,1,rep,name=conditions\"`\n\n\n\t + \ // other fields\n\t}" + properties: + lastTransitionTime: + description: |- + lastTransitionTime is the last time the condition transitioned from one status to another. + This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: |- + message is a human readable message indicating details about the transition. + This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: |- + observedGeneration represents the .metadata.generation that the condition was set based upon. + For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date + with respect to the current state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: |- + reason contains a programmatic identifier indicating the reason for the condition's last transition. + Producers of specific condition types may define expected values and meanings for this field, + and whether the values are considered a guaranteed API. + The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: |- + type of condition in CamelCase or in foo.example.com/CamelCase. + --- + Many .condition.type values are consistent across resources like Available, but because arbitrary conditions can be + useful (see .node.status.conditions), the ability to deconflict is important. + The regex it matches is (dns1123SubdomainFmt/)?(qualifiedNameFmt) + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + x-kubernetes-list-map-keys: + - type + x-kubernetes-list-type: map + phase: + default: Pending + description: Phase summarizes the overall status of the GitHubIdentityProvider. + enum: + - Pending + - Ready + - Error + type: string + type: object + required: + - spec + type: object + served: true + storage: true + subresources: + status: {} diff --git a/generated/1.28/crds/idp.supervisor.pinniped.dev_ldapidentityproviders.yaml b/generated/1.28/crds/idp.supervisor.pinniped.dev_ldapidentityproviders.yaml index f718e93aa..711e9a754 100644 --- a/generated/1.28/crds/idp.supervisor.pinniped.dev_ldapidentityproviders.yaml +++ b/generated/1.28/crds/idp.supervisor.pinniped.dev_ldapidentityproviders.yaml @@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - controller-gen.kubebuilder.io/version: v0.14.0 + controller-gen.kubebuilder.io/version: v0.15.0 name: ldapidentityproviders.idp.supervisor.pinniped.dev spec: group: idp.supervisor.pinniped.dev diff --git a/generated/1.28/crds/idp.supervisor.pinniped.dev_oidcidentityproviders.yaml b/generated/1.28/crds/idp.supervisor.pinniped.dev_oidcidentityproviders.yaml index 486665605..acfca1573 100644 --- a/generated/1.28/crds/idp.supervisor.pinniped.dev_oidcidentityproviders.yaml +++ b/generated/1.28/crds/idp.supervisor.pinniped.dev_oidcidentityproviders.yaml @@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - controller-gen.kubebuilder.io/version: v0.14.0 + controller-gen.kubebuilder.io/version: v0.15.0 name: oidcidentityproviders.idp.supervisor.pinniped.dev spec: group: idp.supervisor.pinniped.dev diff --git a/generated/1.29/README.adoc b/generated/1.29/README.adoc index 6b7cb1923..120952689 100644 --- a/generated/1.29/README.adoc +++ b/generated/1.29/README.adoc @@ -1645,6 +1645,285 @@ Optional, when empty this defaults to "objectGUID". + |=== +[id="{anchor_prefix}-go-pinniped-dev-generated-1-29-apis-supervisor-idp-v1alpha1-githubapiconfig"] +==== GitHubAPIConfig + +GitHubAPIConfig allows configuration for GitHub Enterprise Server + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-29-apis-supervisor-idp-v1alpha1-githubidentityproviderspec[$$GitHubIdentityProviderSpec$$] +**** + +[cols="25a,75a", options="header"] +|=== +| Field | Description +| *`host`* __string__ | Host is required only for GitHub Enterprise Server. + +Defaults to using GitHub's public API ("github.com"). + +Do not specify a protocol or scheme since "https://" will always be used. + +Port is optional. Do not specify a path, query, fragment, or userinfo. + +Only domain name or IP address, subdomains (optional), and port (optional). + +IPv4 and IPv6 are supported. If using an IPv6 address with a port, you must enclose the IPv6 address + +in square brackets. Example: "[::1]:443". + +| *`tls`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-29-apis-supervisor-idp-v1alpha1-tlsspec[$$TLSSpec$$]__ | TLS configuration for GitHub Enterprise Server. + +|=== + + +[id="{anchor_prefix}-go-pinniped-dev-generated-1-29-apis-supervisor-idp-v1alpha1-githuballowauthenticationspec"] +==== GitHubAllowAuthenticationSpec + +GitHubAllowAuthenticationSpec allows customization of who can authenticate using this IDP and how. + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-29-apis-supervisor-idp-v1alpha1-githubidentityproviderspec[$$GitHubIdentityProviderSpec$$] +**** + +[cols="25a,75a", options="header"] +|=== +| Field | Description +| *`organizations`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-29-apis-supervisor-idp-v1alpha1-githuborganizationsspec[$$GitHubOrganizationsSpec$$]__ | Organizations allows customization of which organizations can authenticate using this IDP. + +|=== + + +[id="{anchor_prefix}-go-pinniped-dev-generated-1-29-apis-supervisor-idp-v1alpha1-githuballowedauthorganizationspolicy"] +==== GitHubAllowedAuthOrganizationsPolicy (string) + + + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-29-apis-supervisor-idp-v1alpha1-githuborganizationsspec[$$GitHubOrganizationsSpec$$] +**** + + + +[id="{anchor_prefix}-go-pinniped-dev-generated-1-29-apis-supervisor-idp-v1alpha1-githubclaims"] +==== GitHubClaims + +GitHubClaims allows customization of the username and groups claims. + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-29-apis-supervisor-idp-v1alpha1-githubidentityproviderspec[$$GitHubIdentityProviderSpec$$] +**** + +[cols="25a,75a", options="header"] +|=== +| Field | Description +| *`username`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-29-apis-supervisor-idp-v1alpha1-githubusernameattribute[$$GitHubUsernameAttribute$$]__ | Username configures which property of the GitHub user record shall determine the username in Kubernetes. + + + +Can be either "id", "login", or "login:id". Defaults to "login:id". + + + +GitHub's user login attributes can only contain alphanumeric characters and non-repeating hyphens, + +and may not start or end with hyphens. GitHub users are allowed to change their login name, + +although it is inconvenient. If a GitHub user changed their login name from "foo" to "bar", + +then a second user might change their name from "baz" to "foo" in order to take the old + +username of the first user. For this reason, it is not as safe to make authorization decisions + +based only on the user's login attribute. + + + +If desired, an admin could configure identity transformation expressions on the Pinniped Supervisor's + +FederationDomain to further customize how these usernames are presented to Kubernetes. + + + +Defaults to "login:id", which is the user login attribute, followed by a colon, followed by the unique and + +unchanging integer ID number attribute. This blends human-readable login names with the unchanging ID value + +from GitHub. Colons are not allowed in GitHub login attributes or ID numbers, so this is a reasonable + +choice to concatenate the two values. + + + +See the response schema for + +[Get the authenticated user](https://docs.github.com/en/rest/users/users?apiVersion=2022-11-28#get-the-authenticated-user). + +| *`groups`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-29-apis-supervisor-idp-v1alpha1-githubgroupnameattribute[$$GitHubGroupNameAttribute$$]__ | Groups configures which property of the GitHub team record shall determine the group names in Kubernetes. + + + +Can be either "name" or "slug". Defaults to "slug". + + + +GitHub team names can contain upper and lower case characters, whitespace, and punctuation (e.g. "Kube admins!"). + + + +GitHub team slugs are lower case alphanumeric characters and may contain dashes and underscores (e.g. "kube-admins"). + + + +Group names as presented to Kubernetes will always be prefixed by the GitHub organization name followed by a + +forward slash (e.g. "my-org/my-team"). GitHub organization login names can only contain alphanumeric characters + +or single hyphens, so the first forward slash `/` will be the separator between the organization login name and + +the team name or slug. + + + +If desired, an admin could configure identity transformation expressions on the Pinniped Supervisor's + +FederationDomain to further customize how these group names are presented to Kubernetes. + + + +See the response schema for + +[List teams for the authenticated user](https://docs.github.com/en/rest/teams/teams?apiVersion=2022-11-28#list-teams-for-the-authenticated-user). + +|=== + + +[id="{anchor_prefix}-go-pinniped-dev-generated-1-29-apis-supervisor-idp-v1alpha1-githubclientspec"] +==== GitHubClientSpec + +GitHubClientSpec contains information about the GitHub client that this identity provider will use +for web-based login flows. + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-29-apis-supervisor-idp-v1alpha1-githubidentityproviderspec[$$GitHubIdentityProviderSpec$$] +**** + +[cols="25a,75a", options="header"] +|=== +| Field | Description +| *`secretName`* __string__ | SecretName contains the name of a namespace-local Secret object that provides the clientID and + +clientSecret for an GitHub App or GitHub OAuth2 client. + + + +This secret must be of type "secrets.pinniped.dev/github-client" with keys "clientID" and "clientSecret". + +|=== + + +[id="{anchor_prefix}-go-pinniped-dev-generated-1-29-apis-supervisor-idp-v1alpha1-githubgroupnameattribute"] +==== GitHubGroupNameAttribute (string) + +GitHubGroupNameAttribute allows the user to specify which attribute from GitHub to use for the group +names to present to Kubernetes. See the response schema for +[List teams for the authenticated user](https://docs.github.com/en/rest/teams/teams?apiVersion=2022-11-28#list-teams-for-the-authenticated-user). + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-29-apis-supervisor-idp-v1alpha1-githubclaims[$$GitHubClaims$$] +**** + + + +[id="{anchor_prefix}-go-pinniped-dev-generated-1-29-apis-supervisor-idp-v1alpha1-githubidentityprovider"] +==== GitHubIdentityProvider + +GitHubIdentityProvider describes the configuration of an upstream GitHub identity provider. +This upstream provider can be configured with either a GitHub App or a GitHub OAuth2 App. + + +Right now, only web-based logins are supported, for both the pinniped-cli client and clients configured +as OIDCClients. + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-29-apis-supervisor-idp-v1alpha1-githubidentityproviderlist[$$GitHubIdentityProviderList$$] +**** + +[cols="25a,75a", options="header"] +|=== +| Field | Description +| *`metadata`* __link:https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.29/#objectmeta-v1-meta[$$ObjectMeta$$]__ | Refer to Kubernetes API documentation for fields of `metadata`. + +| *`spec`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-29-apis-supervisor-idp-v1alpha1-githubidentityproviderspec[$$GitHubIdentityProviderSpec$$]__ | Spec for configuring the identity provider. + +| *`status`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-29-apis-supervisor-idp-v1alpha1-githubidentityproviderstatus[$$GitHubIdentityProviderStatus$$]__ | Status of the identity provider. + +|=== + + + + +[id="{anchor_prefix}-go-pinniped-dev-generated-1-29-apis-supervisor-idp-v1alpha1-githubidentityproviderphase"] +==== GitHubIdentityProviderPhase (string) + + + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-29-apis-supervisor-idp-v1alpha1-githubidentityproviderstatus[$$GitHubIdentityProviderStatus$$] +**** + + + +[id="{anchor_prefix}-go-pinniped-dev-generated-1-29-apis-supervisor-idp-v1alpha1-githubidentityproviderspec"] +==== GitHubIdentityProviderSpec + +GitHubIdentityProviderSpec is the spec for configuring an GitHub identity provider. + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-29-apis-supervisor-idp-v1alpha1-githubidentityprovider[$$GitHubIdentityProvider$$] +**** + +[cols="25a,75a", options="header"] +|=== +| Field | Description +| *`githubAPI`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-29-apis-supervisor-idp-v1alpha1-githubapiconfig[$$GitHubAPIConfig$$]__ | GitHubAPI allows configuration for GitHub Enterprise Server + +| *`claims`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-29-apis-supervisor-idp-v1alpha1-githubclaims[$$GitHubClaims$$]__ | Claims allows customization of the username and groups claims. + +| *`allowAuthentication`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-29-apis-supervisor-idp-v1alpha1-githuballowauthenticationspec[$$GitHubAllowAuthenticationSpec$$]__ | AllowAuthentication allows customization of who can authenticate using this IDP and how. + +| *`client`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-29-apis-supervisor-idp-v1alpha1-githubclientspec[$$GitHubClientSpec$$]__ | Client identifies the secret with credentials for a GitHub App or GitHub OAuth2 App (a GitHub client). + +|=== + + +[id="{anchor_prefix}-go-pinniped-dev-generated-1-29-apis-supervisor-idp-v1alpha1-githubidentityproviderstatus"] +==== GitHubIdentityProviderStatus + +GitHubIdentityProviderStatus is the status of an GitHub identity provider. + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-29-apis-supervisor-idp-v1alpha1-githubidentityprovider[$$GitHubIdentityProvider$$] +**** + +[cols="25a,75a", options="header"] +|=== +| Field | Description +| *`phase`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-29-apis-supervisor-idp-v1alpha1-githubidentityproviderphase[$$GitHubIdentityProviderPhase$$]__ | Phase summarizes the overall status of the GitHubIdentityProvider. + +| *`conditions`* __link:https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.29/#condition-v1-meta[$$Condition$$] array__ | Conditions represents the observations of an identity provider's current state. + +|=== + + +[id="{anchor_prefix}-go-pinniped-dev-generated-1-29-apis-supervisor-idp-v1alpha1-githuborganizationsspec"] +==== GitHubOrganizationsSpec + + + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-29-apis-supervisor-idp-v1alpha1-githuballowauthenticationspec[$$GitHubAllowAuthenticationSpec$$] +**** + +[cols="25a,75a", options="header"] +|=== +| Field | Description +| *`policy`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-29-apis-supervisor-idp-v1alpha1-githuballowedauthorganizationspolicy[$$GitHubAllowedAuthOrganizationsPolicy$$]__ | Policy must be set to "AllGitHubUsers" if allowed is empty. + + + +This field only exists to ensure that Pinniped administrators are aware that an empty list of + +allowedOrganizations means all GitHub users are allowed to log in. + +| *`allowed`* __string array__ | Allowed, when specified, indicates that only users with membership in at least one of the listed + +GitHub organizations may log in. In addition, the group membership presented to Kubernetes will only include + +teams within the listed GitHub organizations. Additional login rules or group filtering can optionally be + +provided as policy expression on any Pinniped Supervisor FederationDomain that includes this IDP. + + + +The configured GitHub App or GitHub OAuth App must be allowed to see membership in the listed organizations, + +otherwise Pinniped will not be aware that the user belongs to the listed organization or any teams + +within that organization. + + + +If no organizations are listed, you must set organizations: AllGitHubUsers. + +|=== + + +[id="{anchor_prefix}-go-pinniped-dev-generated-1-29-apis-supervisor-idp-v1alpha1-githubusernameattribute"] +==== GitHubUsernameAttribute (string) + +GitHubUsernameAttribute allows the user to specify which attribute(s) from GitHub to use for the username to present +to Kubernetes. See the response schema for +[Get the authenticated user](https://docs.github.com/en/rest/users/users?apiVersion=2022-11-28#get-the-authenticated-user). + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-29-apis-supervisor-idp-v1alpha1-githubclaims[$$GitHubClaims$$] +**** + + + [id="{anchor_prefix}-go-pinniped-dev-generated-1-29-apis-supervisor-idp-v1alpha1-ldapidentityprovider"] ==== LDAPIdentityProvider @@ -2108,11 +2387,12 @@ Parameter is a key/value pair which represents a parameter in an HTTP request. [id="{anchor_prefix}-go-pinniped-dev-generated-1-29-apis-supervisor-idp-v1alpha1-tlsspec"] ==== TLSSpec -Configuration for TLS parameters related to identity provider integration. +TLSSpec provides TLS configuration for identity provider integration. .Appears In: **** - xref:{anchor_prefix}-go-pinniped-dev-generated-1-29-apis-supervisor-idp-v1alpha1-activedirectoryidentityproviderspec[$$ActiveDirectoryIdentityProviderSpec$$] +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-29-apis-supervisor-idp-v1alpha1-githubapiconfig[$$GitHubAPIConfig$$] - xref:{anchor_prefix}-go-pinniped-dev-generated-1-29-apis-supervisor-idp-v1alpha1-ldapidentityproviderspec[$$LDAPIdentityProviderSpec$$] - xref:{anchor_prefix}-go-pinniped-dev-generated-1-29-apis-supervisor-idp-v1alpha1-oidcidentityproviderspec[$$OIDCIdentityProviderSpec$$] **** diff --git a/generated/1.29/apis/go.mod b/generated/1.29/apis/go.mod index 7d81b6af8..62499ece9 100644 --- a/generated/1.29/apis/go.mod +++ b/generated/1.29/apis/go.mod @@ -4,8 +4,8 @@ module go.pinniped.dev/generated/1.29/apis go 1.21 require ( - k8s.io/api v0.29.4 - k8s.io/apimachinery v0.29.4 + k8s.io/api v0.29.5 + k8s.io/apimachinery v0.29.5 ) require ( diff --git a/generated/1.29/apis/go.sum b/generated/1.29/apis/go.sum index 77d507521..049a3ce3b 100644 --- a/generated/1.29/apis/go.sum +++ b/generated/1.29/apis/go.sum @@ -75,10 +75,10 @@ gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -k8s.io/api v0.29.4 h1:WEnF/XdxuCxdG3ayHNRR8yH3cI1B/llkWBma6bq4R3w= -k8s.io/api v0.29.4/go.mod h1:DetSv0t4FBTcEpfA84NJV3g9a7+rSzlUHk5ADAYHUv0= -k8s.io/apimachinery v0.29.4 h1:RaFdJiDmuKs/8cm1M6Dh1Kvyh59YQFDcFuFTSmXes6Q= -k8s.io/apimachinery v0.29.4/go.mod h1:i3FJVwhvSp/6n8Fl4K97PJEP8C+MM+aoDq4+ZJBf70Y= +k8s.io/api v0.29.5 h1:levS+umUigHCfI3riD36pMY1vQEbrzh4r1ivVWAhHaI= +k8s.io/api v0.29.5/go.mod h1:7b18TtPcJzdjk7w5zWyIHgoAtpGeRvGGASxlS7UZXdQ= +k8s.io/apimachinery v0.29.5 h1:Hofa2BmPfpoT+IyDTlcPdCHSnHtEQMoJYGVoQpRTfv4= +k8s.io/apimachinery v0.29.5/go.mod h1:i3FJVwhvSp/6n8Fl4K97PJEP8C+MM+aoDq4+ZJBf70Y= k8s.io/klog/v2 v2.110.1 h1:U/Af64HJf7FcwMcXyKm2RPM22WZzyR7OSpYj5tg3cL0= k8s.io/klog/v2 v2.110.1/go.mod h1:YGtd1984u+GgbuZ7e08/yBuAfKLSO0+uR1Fhi6ExXjo= k8s.io/utils v0.0.0-20230726121419-3b25d923346b h1:sgn3ZU783SCgtaSJjpcVVlRqd6GSnlTLKgpAAttJvpI= diff --git a/generated/1.29/apis/supervisor/idp/v1alpha1/register.go b/generated/1.29/apis/supervisor/idp/v1alpha1/register.go index 8829a8638..705be8076 100644 --- a/generated/1.29/apis/supervisor/idp/v1alpha1/register.go +++ b/generated/1.29/apis/supervisor/idp/v1alpha1/register.go @@ -1,4 +1,4 @@ -// Copyright 2020-2022 the Pinniped contributors. All Rights Reserved. +// Copyright 2020-2024 the Pinniped contributors. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 package v1alpha1 @@ -36,6 +36,8 @@ func addKnownTypes(scheme *runtime.Scheme) error { &LDAPIdentityProviderList{}, &ActiveDirectoryIdentityProvider{}, &ActiveDirectoryIdentityProviderList{}, + &GitHubIdentityProvider{}, + &GitHubIdentityProviderList{}, ) metav1.AddToGroupVersion(scheme, SchemeGroupVersion) return nil diff --git a/generated/1.29/apis/supervisor/idp/v1alpha1/types_githubidentityprovider.go b/generated/1.29/apis/supervisor/idp/v1alpha1/types_githubidentityprovider.go new file mode 100644 index 000000000..c84f46dbd --- /dev/null +++ b/generated/1.29/apis/supervisor/idp/v1alpha1/types_githubidentityprovider.go @@ -0,0 +1,256 @@ +// Copyright 2024 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package v1alpha1 + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +type GitHubIdentityProviderPhase string + +const ( + // GitHubPhasePending is the default phase for newly-created GitHubIdentityProvider resources. + GitHubPhasePending GitHubIdentityProviderPhase = "Pending" + + // GitHubPhaseReady is the phase for an GitHubIdentityProvider resource in a healthy state. + GitHubPhaseReady GitHubIdentityProviderPhase = "Ready" + + // GitHubPhaseError is the phase for an GitHubIdentityProvider in an unhealthy state. + GitHubPhaseError GitHubIdentityProviderPhase = "Error" +) + +type GitHubAllowedAuthOrganizationsPolicy string + +const ( + // GitHubAllowedAuthOrganizationsPolicyAllGitHubUsers means any GitHub user is allowed to log in using this identity + // provider, regardless of their organization membership or lack thereof. + GitHubAllowedAuthOrganizationsPolicyAllGitHubUsers GitHubAllowedAuthOrganizationsPolicy = "AllGitHubUsers" + + // GitHubAllowedAuthOrganizationsPolicyOnlyUsersFromAllowedOrganizations means only those users with membership in + // the listed GitHub organizations are allowed to log in. + GitHubAllowedAuthOrganizationsPolicyOnlyUsersFromAllowedOrganizations GitHubAllowedAuthOrganizationsPolicy = "OnlyUsersFromAllowedOrganizations" +) + +// GitHubIdentityProviderStatus is the status of an GitHub identity provider. +type GitHubIdentityProviderStatus struct { + // Phase summarizes the overall status of the GitHubIdentityProvider. + // + // +kubebuilder:default=Pending + // +kubebuilder:validation:Enum=Pending;Ready;Error + Phase GitHubIdentityProviderPhase `json:"phase,omitempty"` + + // Conditions represents the observations of an identity provider's current state. + // + // +patchMergeKey=type + // +patchStrategy=merge + // +listType=map + // +listMapKey=type + Conditions []metav1.Condition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type"` +} + +// GitHubAPIConfig allows configuration for GitHub Enterprise Server +type GitHubAPIConfig struct { + // Host is required only for GitHub Enterprise Server. + // Defaults to using GitHub's public API ("github.com"). + // Do not specify a protocol or scheme since "https://" will always be used. + // Port is optional. Do not specify a path, query, fragment, or userinfo. + // Only domain name or IP address, subdomains (optional), and port (optional). + // IPv4 and IPv6 are supported. If using an IPv6 address with a port, you must enclose the IPv6 address + // in square brackets. Example: "[::1]:443". + // + // +kubebuilder:default="github.com" + // +kubebuilder:validation:MinLength=1 + // +optional + Host *string `json:"host"` + + // TLS configuration for GitHub Enterprise Server. + // + // +optional + TLS *TLSSpec `json:"tls,omitempty"` +} + +// GitHubUsernameAttribute allows the user to specify which attribute(s) from GitHub to use for the username to present +// to Kubernetes. See the response schema for +// [Get the authenticated user](https://docs.github.com/en/rest/users/users?apiVersion=2022-11-28#get-the-authenticated-user). +type GitHubUsernameAttribute string + +const ( + // GitHubUsernameID specifies using the `id` attribute from the GitHub user for the username to present to Kubernetes. + GitHubUsernameID GitHubUsernameAttribute = "id" + + // GitHubUsernameLogin specifies using the `login` attribute from the GitHub user as the username to present to Kubernetes. + GitHubUsernameLogin GitHubUsernameAttribute = "login" + + // GitHubUsernameLoginAndID specifies combining the `login` and `id` attributes from the GitHub user as the + // username to present to Kubernetes, separated by a colon. Example: "my-login:1234" + GitHubUsernameLoginAndID GitHubUsernameAttribute = "login:id" +) + +// GitHubGroupNameAttribute allows the user to specify which attribute from GitHub to use for the group +// names to present to Kubernetes. See the response schema for +// [List teams for the authenticated user](https://docs.github.com/en/rest/teams/teams?apiVersion=2022-11-28#list-teams-for-the-authenticated-user). +type GitHubGroupNameAttribute string + +const ( + // GitHubUseTeamNameForGroupName specifies using the GitHub team's `name` attribute as the group name to present to Kubernetes. + GitHubUseTeamNameForGroupName GitHubGroupNameAttribute = "name" + + // GitHubUseTeamSlugForGroupName specifies using the GitHub team's `slug` attribute as the group name to present to Kubernetes. + GitHubUseTeamSlugForGroupName GitHubGroupNameAttribute = "slug" +) + +// GitHubClaims allows customization of the username and groups claims. +type GitHubClaims struct { + // Username configures which property of the GitHub user record shall determine the username in Kubernetes. + // + // Can be either "id", "login", or "login:id". Defaults to "login:id". + // + // GitHub's user login attributes can only contain alphanumeric characters and non-repeating hyphens, + // and may not start or end with hyphens. GitHub users are allowed to change their login name, + // although it is inconvenient. If a GitHub user changed their login name from "foo" to "bar", + // then a second user might change their name from "baz" to "foo" in order to take the old + // username of the first user. For this reason, it is not as safe to make authorization decisions + // based only on the user's login attribute. + // + // If desired, an admin could configure identity transformation expressions on the Pinniped Supervisor's + // FederationDomain to further customize how these usernames are presented to Kubernetes. + // + // Defaults to "login:id", which is the user login attribute, followed by a colon, followed by the unique and + // unchanging integer ID number attribute. This blends human-readable login names with the unchanging ID value + // from GitHub. Colons are not allowed in GitHub login attributes or ID numbers, so this is a reasonable + // choice to concatenate the two values. + // + // See the response schema for + // [Get the authenticated user](https://docs.github.com/en/rest/users/users?apiVersion=2022-11-28#get-the-authenticated-user). + // + // +kubebuilder:default="login:id" + // +kubebuilder:validation:Enum={"id","login","login:id"} + // +optional + Username *GitHubUsernameAttribute `json:"username"` + + // Groups configures which property of the GitHub team record shall determine the group names in Kubernetes. + // + // Can be either "name" or "slug". Defaults to "slug". + // + // GitHub team names can contain upper and lower case characters, whitespace, and punctuation (e.g. "Kube admins!"). + // + // GitHub team slugs are lower case alphanumeric characters and may contain dashes and underscores (e.g. "kube-admins"). + // + // Group names as presented to Kubernetes will always be prefixed by the GitHub organization name followed by a + // forward slash (e.g. "my-org/my-team"). GitHub organization login names can only contain alphanumeric characters + // or single hyphens, so the first forward slash `/` will be the separator between the organization login name and + // the team name or slug. + // + // If desired, an admin could configure identity transformation expressions on the Pinniped Supervisor's + // FederationDomain to further customize how these group names are presented to Kubernetes. + // + // See the response schema for + // [List teams for the authenticated user](https://docs.github.com/en/rest/teams/teams?apiVersion=2022-11-28#list-teams-for-the-authenticated-user). + // + // +kubebuilder:default=slug + // +kubebuilder:validation:Enum=name;slug + // +optional + Groups *GitHubGroupNameAttribute `json:"groups"` +} + +// GitHubClientSpec contains information about the GitHub client that this identity provider will use +// for web-based login flows. +type GitHubClientSpec struct { + // SecretName contains the name of a namespace-local Secret object that provides the clientID and + // clientSecret for an GitHub App or GitHub OAuth2 client. + // + // This secret must be of type "secrets.pinniped.dev/github-client" with keys "clientID" and "clientSecret". + // + // +kubebuilder:validation:MinLength=1 + SecretName string `json:"secretName"` +} + +type GitHubOrganizationsSpec struct { + // Policy must be set to "AllGitHubUsers" if allowed is empty. + // + // This field only exists to ensure that Pinniped administrators are aware that an empty list of + // allowedOrganizations means all GitHub users are allowed to log in. + // + // +kubebuilder:default=OnlyUsersFromAllowedOrganizations + // +kubebuilder:validation:Enum=OnlyUsersFromAllowedOrganizations;AllGitHubUsers + // +optional + Policy *GitHubAllowedAuthOrganizationsPolicy `json:"policy"` + + // Allowed, when specified, indicates that only users with membership in at least one of the listed + // GitHub organizations may log in. In addition, the group membership presented to Kubernetes will only include + // teams within the listed GitHub organizations. Additional login rules or group filtering can optionally be + // provided as policy expression on any Pinniped Supervisor FederationDomain that includes this IDP. + // + // The configured GitHub App or GitHub OAuth App must be allowed to see membership in the listed organizations, + // otherwise Pinniped will not be aware that the user belongs to the listed organization or any teams + // within that organization. + // + // If no organizations are listed, you must set organizations: AllGitHubUsers. + // + // +kubebuilder:validation:MaxItems=64 + // +listType=set + // +optional + Allowed []string `json:"allowed,omitempty"` +} + +// GitHubAllowAuthenticationSpec allows customization of who can authenticate using this IDP and how. +type GitHubAllowAuthenticationSpec struct { + // Organizations allows customization of which organizations can authenticate using this IDP. + // +kubebuilder:validation:XValidation:message="spec.allowAuthentication.organizations.policy must be 'OnlyUsersFromAllowedOrganizations' when spec.allowAuthentication.organizations.allowed has organizations listed",rule="!(has(self.allowed) && size(self.allowed) > 0 && self.policy == 'AllGitHubUsers')" + // +kubebuilder:validation:XValidation:message="spec.allowAuthentication.organizations.policy must be 'AllGitHubUsers' when spec.allowAuthentication.organizations.allowed is empty",rule="!((!has(self.allowed) || size(self.allowed) == 0) && self.policy == 'OnlyUsersFromAllowedOrganizations')" + Organizations GitHubOrganizationsSpec `json:"organizations"` +} + +// GitHubIdentityProviderSpec is the spec for configuring an GitHub identity provider. +type GitHubIdentityProviderSpec struct { + // GitHubAPI allows configuration for GitHub Enterprise Server + // + // +kubebuilder:default={} + GitHubAPI GitHubAPIConfig `json:"githubAPI,omitempty"` + + // Claims allows customization of the username and groups claims. + // + // +kubebuilder:default={} + Claims GitHubClaims `json:"claims,omitempty"` + + // AllowAuthentication allows customization of who can authenticate using this IDP and how. + AllowAuthentication GitHubAllowAuthenticationSpec `json:"allowAuthentication"` + + // Client identifies the secret with credentials for a GitHub App or GitHub OAuth2 App (a GitHub client). + Client GitHubClientSpec `json:"client"` +} + +// GitHubIdentityProvider describes the configuration of an upstream GitHub identity provider. +// This upstream provider can be configured with either a GitHub App or a GitHub OAuth2 App. +// +// Right now, only web-based logins are supported, for both the pinniped-cli client and clients configured +// as OIDCClients. +// +// +genclient +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object +// +kubebuilder:resource:categories=pinniped;pinniped-idp;pinniped-idps +// +kubebuilder:printcolumn:name="Host",type=string,JSONPath=`.spec.githubAPI.host` +// +kubebuilder:printcolumn:name="Status",type=string,JSONPath=`.status.phase` +// +kubebuilder:printcolumn:name="Age",type=date,JSONPath=`.metadata.creationTimestamp` +// +kubebuilder:subresource:status +type GitHubIdentityProvider struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + // Spec for configuring the identity provider. + Spec GitHubIdentityProviderSpec `json:"spec"` + + // Status of the identity provider. + Status GitHubIdentityProviderStatus `json:"status,omitempty"` +} + +// GitHubIdentityProviderList lists GitHubIdentityProvider objects. +// +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object +type GitHubIdentityProviderList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + + Items []GitHubIdentityProvider `json:"items"` +} diff --git a/generated/1.29/apis/supervisor/idp/v1alpha1/types_tls.go b/generated/1.29/apis/supervisor/idp/v1alpha1/types_tls.go index 1413a262c..49b49373c 100644 --- a/generated/1.29/apis/supervisor/idp/v1alpha1/types_tls.go +++ b/generated/1.29/apis/supervisor/idp/v1alpha1/types_tls.go @@ -1,9 +1,9 @@ -// Copyright 2020-2022 the Pinniped contributors. All Rights Reserved. +// Copyright 2020-2024 the Pinniped contributors. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 package v1alpha1 -// Configuration for TLS parameters related to identity provider integration. +// TLSSpec provides TLS configuration for identity provider integration. type TLSSpec struct { // X.509 Certificate Authority (base64-encoded PEM bundle). If omitted, a default set of system roots will be trusted. // +optional diff --git a/generated/1.29/apis/supervisor/idp/v1alpha1/zz_generated.deepcopy.go b/generated/1.29/apis/supervisor/idp/v1alpha1/zz_generated.deepcopy.go index b0cb168b5..e48860e82 100644 --- a/generated/1.29/apis/supervisor/idp/v1alpha1/zz_generated.deepcopy.go +++ b/generated/1.29/apis/supervisor/idp/v1alpha1/zz_generated.deepcopy.go @@ -203,6 +203,221 @@ func (in *ActiveDirectoryIdentityProviderUserSearchAttributes) DeepCopy() *Activ return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *GitHubAPIConfig) DeepCopyInto(out *GitHubAPIConfig) { + *out = *in + if in.Host != nil { + in, out := &in.Host, &out.Host + *out = new(string) + **out = **in + } + if in.TLS != nil { + in, out := &in.TLS, &out.TLS + *out = new(TLSSpec) + **out = **in + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GitHubAPIConfig. +func (in *GitHubAPIConfig) DeepCopy() *GitHubAPIConfig { + if in == nil { + return nil + } + out := new(GitHubAPIConfig) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *GitHubAllowAuthenticationSpec) DeepCopyInto(out *GitHubAllowAuthenticationSpec) { + *out = *in + in.Organizations.DeepCopyInto(&out.Organizations) + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GitHubAllowAuthenticationSpec. +func (in *GitHubAllowAuthenticationSpec) DeepCopy() *GitHubAllowAuthenticationSpec { + if in == nil { + return nil + } + out := new(GitHubAllowAuthenticationSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *GitHubClaims) DeepCopyInto(out *GitHubClaims) { + *out = *in + if in.Username != nil { + in, out := &in.Username, &out.Username + *out = new(GitHubUsernameAttribute) + **out = **in + } + if in.Groups != nil { + in, out := &in.Groups, &out.Groups + *out = new(GitHubGroupNameAttribute) + **out = **in + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GitHubClaims. +func (in *GitHubClaims) DeepCopy() *GitHubClaims { + if in == nil { + return nil + } + out := new(GitHubClaims) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *GitHubClientSpec) DeepCopyInto(out *GitHubClientSpec) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GitHubClientSpec. +func (in *GitHubClientSpec) DeepCopy() *GitHubClientSpec { + if in == nil { + return nil + } + out := new(GitHubClientSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *GitHubIdentityProvider) DeepCopyInto(out *GitHubIdentityProvider) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + in.Status.DeepCopyInto(&out.Status) + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GitHubIdentityProvider. +func (in *GitHubIdentityProvider) DeepCopy() *GitHubIdentityProvider { + if in == nil { + return nil + } + out := new(GitHubIdentityProvider) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *GitHubIdentityProvider) 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 *GitHubIdentityProviderList) DeepCopyInto(out *GitHubIdentityProviderList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]GitHubIdentityProvider, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GitHubIdentityProviderList. +func (in *GitHubIdentityProviderList) DeepCopy() *GitHubIdentityProviderList { + if in == nil { + return nil + } + out := new(GitHubIdentityProviderList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *GitHubIdentityProviderList) 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 *GitHubIdentityProviderSpec) DeepCopyInto(out *GitHubIdentityProviderSpec) { + *out = *in + in.GitHubAPI.DeepCopyInto(&out.GitHubAPI) + in.Claims.DeepCopyInto(&out.Claims) + in.AllowAuthentication.DeepCopyInto(&out.AllowAuthentication) + out.Client = in.Client + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GitHubIdentityProviderSpec. +func (in *GitHubIdentityProviderSpec) DeepCopy() *GitHubIdentityProviderSpec { + if in == nil { + return nil + } + out := new(GitHubIdentityProviderSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *GitHubIdentityProviderStatus) DeepCopyInto(out *GitHubIdentityProviderStatus) { + *out = *in + if in.Conditions != nil { + in, out := &in.Conditions, &out.Conditions + *out = make([]v1.Condition, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GitHubIdentityProviderStatus. +func (in *GitHubIdentityProviderStatus) DeepCopy() *GitHubIdentityProviderStatus { + if in == nil { + return nil + } + out := new(GitHubIdentityProviderStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *GitHubOrganizationsSpec) DeepCopyInto(out *GitHubOrganizationsSpec) { + *out = *in + if in.Policy != nil { + in, out := &in.Policy, &out.Policy + *out = new(GitHubAllowedAuthOrganizationsPolicy) + **out = **in + } + if in.Allowed != nil { + in, out := &in.Allowed, &out.Allowed + *out = make([]string, len(*in)) + copy(*out, *in) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GitHubOrganizationsSpec. +func (in *GitHubOrganizationsSpec) DeepCopy() *GitHubOrganizationsSpec { + if in == nil { + return nil + } + out := new(GitHubOrganizationsSpec) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *LDAPIdentityProvider) DeepCopyInto(out *LDAPIdentityProvider) { *out = *in diff --git a/generated/1.29/apis/supervisor/idpdiscovery/v1alpha1/types_supervisor_idp_discovery.go b/generated/1.29/apis/supervisor/idpdiscovery/v1alpha1/types_supervisor_idp_discovery.go index 837fed2dd..11be6f9dc 100644 --- a/generated/1.29/apis/supervisor/idpdiscovery/v1alpha1/types_supervisor_idp_discovery.go +++ b/generated/1.29/apis/supervisor/idpdiscovery/v1alpha1/types_supervisor_idp_discovery.go @@ -15,6 +15,7 @@ const ( IDPTypeOIDC IDPType = "oidc" IDPTypeLDAP IDPType = "ldap" IDPTypeActiveDirectory IDPType = "activedirectory" + IDPTypeGitHub IDPType = "github" IDPFlowCLIPassword IDPFlow = "cli_password" IDPFlowBrowserAuthcode IDPFlow = "browser_authcode" diff --git a/generated/1.29/client/go.mod b/generated/1.29/client/go.mod index a7dbf4f5d..3b5d17359 100644 --- a/generated/1.29/client/go.mod +++ b/generated/1.29/client/go.mod @@ -7,8 +7,8 @@ replace go.pinniped.dev/generated/1.29/apis => ../apis require ( go.pinniped.dev/generated/1.29/apis v0.0.0 - k8s.io/apimachinery v0.29.4 - k8s.io/client-go v0.29.4 + k8s.io/apimachinery v0.29.5 + k8s.io/client-go v0.29.5 k8s.io/kube-openapi v0.0.0-20231010175941-2dd684a91f00 ) @@ -44,7 +44,7 @@ require ( gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect - k8s.io/api v0.29.4 // indirect + k8s.io/api v0.29.5 // indirect k8s.io/klog/v2 v2.110.1 // indirect k8s.io/utils v0.0.0-20230726121419-3b25d923346b // indirect sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect diff --git a/generated/1.29/client/go.sum b/generated/1.29/client/go.sum index 2e92347b2..c586b65a6 100644 --- a/generated/1.29/client/go.sum +++ b/generated/1.29/client/go.sum @@ -134,12 +134,12 @@ gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -k8s.io/api v0.29.4 h1:WEnF/XdxuCxdG3ayHNRR8yH3cI1B/llkWBma6bq4R3w= -k8s.io/api v0.29.4/go.mod h1:DetSv0t4FBTcEpfA84NJV3g9a7+rSzlUHk5ADAYHUv0= -k8s.io/apimachinery v0.29.4 h1:RaFdJiDmuKs/8cm1M6Dh1Kvyh59YQFDcFuFTSmXes6Q= -k8s.io/apimachinery v0.29.4/go.mod h1:i3FJVwhvSp/6n8Fl4K97PJEP8C+MM+aoDq4+ZJBf70Y= -k8s.io/client-go v0.29.4 h1:79ytIedxVfyXV8rpH3jCBW0u+un0fxHDwX5F9K8dPR8= -k8s.io/client-go v0.29.4/go.mod h1:kC1thZQ4zQWYwldsfI088BbK6RkxK+aF5ebV8y9Q4tk= +k8s.io/api v0.29.5 h1:levS+umUigHCfI3riD36pMY1vQEbrzh4r1ivVWAhHaI= +k8s.io/api v0.29.5/go.mod h1:7b18TtPcJzdjk7w5zWyIHgoAtpGeRvGGASxlS7UZXdQ= +k8s.io/apimachinery v0.29.5 h1:Hofa2BmPfpoT+IyDTlcPdCHSnHtEQMoJYGVoQpRTfv4= +k8s.io/apimachinery v0.29.5/go.mod h1:i3FJVwhvSp/6n8Fl4K97PJEP8C+MM+aoDq4+ZJBf70Y= +k8s.io/client-go v0.29.5 h1:nlASXmPQy190qTteaVP31g3c/wi2kycznkTP7Sv1zPc= +k8s.io/client-go v0.29.5/go.mod h1:aY5CnqUUvXYccJhm47XHoPcRyX6vouHdIBHaKZGTbK4= k8s.io/klog/v2 v2.110.1 h1:U/Af64HJf7FcwMcXyKm2RPM22WZzyR7OSpYj5tg3cL0= k8s.io/klog/v2 v2.110.1/go.mod h1:YGtd1984u+GgbuZ7e08/yBuAfKLSO0+uR1Fhi6ExXjo= k8s.io/kube-openapi v0.0.0-20231010175941-2dd684a91f00 h1:aVUu9fTY98ivBPKR9Y5w/AuzbMm96cd3YHRTU83I780= diff --git a/generated/1.29/client/supervisor/clientset/versioned/typed/idp/v1alpha1/fake/fake_githubidentityprovider.go b/generated/1.29/client/supervisor/clientset/versioned/typed/idp/v1alpha1/fake/fake_githubidentityprovider.go new file mode 100644 index 000000000..67b4c883a --- /dev/null +++ b/generated/1.29/client/supervisor/clientset/versioned/typed/idp/v1alpha1/fake/fake_githubidentityprovider.go @@ -0,0 +1,128 @@ +// Copyright 2020-2024 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +// Code generated by client-gen. DO NOT EDIT. + +package fake + +import ( + "context" + + v1alpha1 "go.pinniped.dev/generated/1.29/apis/supervisor/idp/v1alpha1" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + labels "k8s.io/apimachinery/pkg/labels" + types "k8s.io/apimachinery/pkg/types" + watch "k8s.io/apimachinery/pkg/watch" + testing "k8s.io/client-go/testing" +) + +// FakeGitHubIdentityProviders implements GitHubIdentityProviderInterface +type FakeGitHubIdentityProviders struct { + Fake *FakeIDPV1alpha1 + ns string +} + +var githubidentityprovidersResource = v1alpha1.SchemeGroupVersion.WithResource("githubidentityproviders") + +var githubidentityprovidersKind = v1alpha1.SchemeGroupVersion.WithKind("GitHubIdentityProvider") + +// Get takes name of the gitHubIdentityProvider, and returns the corresponding gitHubIdentityProvider object, and an error if there is any. +func (c *FakeGitHubIdentityProviders) Get(ctx context.Context, name string, options v1.GetOptions) (result *v1alpha1.GitHubIdentityProvider, err error) { + obj, err := c.Fake. + Invokes(testing.NewGetAction(githubidentityprovidersResource, c.ns, name), &v1alpha1.GitHubIdentityProvider{}) + + if obj == nil { + return nil, err + } + return obj.(*v1alpha1.GitHubIdentityProvider), err +} + +// List takes label and field selectors, and returns the list of GitHubIdentityProviders that match those selectors. +func (c *FakeGitHubIdentityProviders) List(ctx context.Context, opts v1.ListOptions) (result *v1alpha1.GitHubIdentityProviderList, err error) { + obj, err := c.Fake. + Invokes(testing.NewListAction(githubidentityprovidersResource, githubidentityprovidersKind, c.ns, opts), &v1alpha1.GitHubIdentityProviderList{}) + + if obj == nil { + return nil, err + } + + label, _, _ := testing.ExtractFromListOptions(opts) + if label == nil { + label = labels.Everything() + } + list := &v1alpha1.GitHubIdentityProviderList{ListMeta: obj.(*v1alpha1.GitHubIdentityProviderList).ListMeta} + for _, item := range obj.(*v1alpha1.GitHubIdentityProviderList).Items { + if label.Matches(labels.Set(item.Labels)) { + list.Items = append(list.Items, item) + } + } + return list, err +} + +// Watch returns a watch.Interface that watches the requested gitHubIdentityProviders. +func (c *FakeGitHubIdentityProviders) Watch(ctx context.Context, opts v1.ListOptions) (watch.Interface, error) { + return c.Fake. + InvokesWatch(testing.NewWatchAction(githubidentityprovidersResource, c.ns, opts)) + +} + +// Create takes the representation of a gitHubIdentityProvider and creates it. Returns the server's representation of the gitHubIdentityProvider, and an error, if there is any. +func (c *FakeGitHubIdentityProviders) Create(ctx context.Context, gitHubIdentityProvider *v1alpha1.GitHubIdentityProvider, opts v1.CreateOptions) (result *v1alpha1.GitHubIdentityProvider, err error) { + obj, err := c.Fake. + Invokes(testing.NewCreateAction(githubidentityprovidersResource, c.ns, gitHubIdentityProvider), &v1alpha1.GitHubIdentityProvider{}) + + if obj == nil { + return nil, err + } + return obj.(*v1alpha1.GitHubIdentityProvider), err +} + +// Update takes the representation of a gitHubIdentityProvider and updates it. Returns the server's representation of the gitHubIdentityProvider, and an error, if there is any. +func (c *FakeGitHubIdentityProviders) Update(ctx context.Context, gitHubIdentityProvider *v1alpha1.GitHubIdentityProvider, opts v1.UpdateOptions) (result *v1alpha1.GitHubIdentityProvider, err error) { + obj, err := c.Fake. + Invokes(testing.NewUpdateAction(githubidentityprovidersResource, c.ns, gitHubIdentityProvider), &v1alpha1.GitHubIdentityProvider{}) + + if obj == nil { + return nil, err + } + return obj.(*v1alpha1.GitHubIdentityProvider), err +} + +// UpdateStatus was generated because the type contains a Status member. +// Add a +genclient:noStatus comment above the type to avoid generating UpdateStatus(). +func (c *FakeGitHubIdentityProviders) UpdateStatus(ctx context.Context, gitHubIdentityProvider *v1alpha1.GitHubIdentityProvider, opts v1.UpdateOptions) (*v1alpha1.GitHubIdentityProvider, error) { + obj, err := c.Fake. + Invokes(testing.NewUpdateSubresourceAction(githubidentityprovidersResource, "status", c.ns, gitHubIdentityProvider), &v1alpha1.GitHubIdentityProvider{}) + + if obj == nil { + return nil, err + } + return obj.(*v1alpha1.GitHubIdentityProvider), err +} + +// Delete takes name of the gitHubIdentityProvider and deletes it. Returns an error if one occurs. +func (c *FakeGitHubIdentityProviders) Delete(ctx context.Context, name string, opts v1.DeleteOptions) error { + _, err := c.Fake. + Invokes(testing.NewDeleteActionWithOptions(githubidentityprovidersResource, c.ns, name, opts), &v1alpha1.GitHubIdentityProvider{}) + + return err +} + +// DeleteCollection deletes a collection of objects. +func (c *FakeGitHubIdentityProviders) DeleteCollection(ctx context.Context, opts v1.DeleteOptions, listOpts v1.ListOptions) error { + action := testing.NewDeleteCollectionAction(githubidentityprovidersResource, c.ns, listOpts) + + _, err := c.Fake.Invokes(action, &v1alpha1.GitHubIdentityProviderList{}) + return err +} + +// Patch applies the patch and returns the patched gitHubIdentityProvider. +func (c *FakeGitHubIdentityProviders) Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts v1.PatchOptions, subresources ...string) (result *v1alpha1.GitHubIdentityProvider, err error) { + obj, err := c.Fake. + Invokes(testing.NewPatchSubresourceAction(githubidentityprovidersResource, c.ns, name, pt, data, subresources...), &v1alpha1.GitHubIdentityProvider{}) + + if obj == nil { + return nil, err + } + return obj.(*v1alpha1.GitHubIdentityProvider), err +} diff --git a/generated/1.29/client/supervisor/clientset/versioned/typed/idp/v1alpha1/fake/fake_idp_client.go b/generated/1.29/client/supervisor/clientset/versioned/typed/idp/v1alpha1/fake/fake_idp_client.go index b171331a5..683018c4a 100644 --- a/generated/1.29/client/supervisor/clientset/versioned/typed/idp/v1alpha1/fake/fake_idp_client.go +++ b/generated/1.29/client/supervisor/clientset/versioned/typed/idp/v1alpha1/fake/fake_idp_client.go @@ -19,6 +19,10 @@ func (c *FakeIDPV1alpha1) ActiveDirectoryIdentityProviders(namespace string) v1a return &FakeActiveDirectoryIdentityProviders{c, namespace} } +func (c *FakeIDPV1alpha1) GitHubIdentityProviders(namespace string) v1alpha1.GitHubIdentityProviderInterface { + return &FakeGitHubIdentityProviders{c, namespace} +} + func (c *FakeIDPV1alpha1) LDAPIdentityProviders(namespace string) v1alpha1.LDAPIdentityProviderInterface { return &FakeLDAPIdentityProviders{c, namespace} } diff --git a/generated/1.29/client/supervisor/clientset/versioned/typed/idp/v1alpha1/generated_expansion.go b/generated/1.29/client/supervisor/clientset/versioned/typed/idp/v1alpha1/generated_expansion.go index 79be098d6..a7acc8120 100644 --- a/generated/1.29/client/supervisor/clientset/versioned/typed/idp/v1alpha1/generated_expansion.go +++ b/generated/1.29/client/supervisor/clientset/versioned/typed/idp/v1alpha1/generated_expansion.go @@ -7,6 +7,8 @@ package v1alpha1 type ActiveDirectoryIdentityProviderExpansion interface{} +type GitHubIdentityProviderExpansion interface{} + type LDAPIdentityProviderExpansion interface{} type OIDCIdentityProviderExpansion interface{} diff --git a/generated/1.29/client/supervisor/clientset/versioned/typed/idp/v1alpha1/githubidentityprovider.go b/generated/1.29/client/supervisor/clientset/versioned/typed/idp/v1alpha1/githubidentityprovider.go new file mode 100644 index 000000000..6bb834703 --- /dev/null +++ b/generated/1.29/client/supervisor/clientset/versioned/typed/idp/v1alpha1/githubidentityprovider.go @@ -0,0 +1,182 @@ +// Copyright 2020-2024 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +// Code generated by client-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + "context" + "time" + + v1alpha1 "go.pinniped.dev/generated/1.29/apis/supervisor/idp/v1alpha1" + scheme "go.pinniped.dev/generated/1.29/client/supervisor/clientset/versioned/scheme" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + types "k8s.io/apimachinery/pkg/types" + watch "k8s.io/apimachinery/pkg/watch" + rest "k8s.io/client-go/rest" +) + +// GitHubIdentityProvidersGetter has a method to return a GitHubIdentityProviderInterface. +// A group's client should implement this interface. +type GitHubIdentityProvidersGetter interface { + GitHubIdentityProviders(namespace string) GitHubIdentityProviderInterface +} + +// GitHubIdentityProviderInterface has methods to work with GitHubIdentityProvider resources. +type GitHubIdentityProviderInterface interface { + Create(ctx context.Context, gitHubIdentityProvider *v1alpha1.GitHubIdentityProvider, opts v1.CreateOptions) (*v1alpha1.GitHubIdentityProvider, error) + Update(ctx context.Context, gitHubIdentityProvider *v1alpha1.GitHubIdentityProvider, opts v1.UpdateOptions) (*v1alpha1.GitHubIdentityProvider, error) + UpdateStatus(ctx context.Context, gitHubIdentityProvider *v1alpha1.GitHubIdentityProvider, opts v1.UpdateOptions) (*v1alpha1.GitHubIdentityProvider, 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) (*v1alpha1.GitHubIdentityProvider, error) + List(ctx context.Context, opts v1.ListOptions) (*v1alpha1.GitHubIdentityProviderList, 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 *v1alpha1.GitHubIdentityProvider, err error) + GitHubIdentityProviderExpansion +} + +// gitHubIdentityProviders implements GitHubIdentityProviderInterface +type gitHubIdentityProviders struct { + client rest.Interface + ns string +} + +// newGitHubIdentityProviders returns a GitHubIdentityProviders +func newGitHubIdentityProviders(c *IDPV1alpha1Client, namespace string) *gitHubIdentityProviders { + return &gitHubIdentityProviders{ + client: c.RESTClient(), + ns: namespace, + } +} + +// Get takes name of the gitHubIdentityProvider, and returns the corresponding gitHubIdentityProvider object, and an error if there is any. +func (c *gitHubIdentityProviders) Get(ctx context.Context, name string, options v1.GetOptions) (result *v1alpha1.GitHubIdentityProvider, err error) { + result = &v1alpha1.GitHubIdentityProvider{} + err = c.client.Get(). + Namespace(c.ns). + Resource("githubidentityproviders"). + Name(name). + VersionedParams(&options, scheme.ParameterCodec). + Do(ctx). + Into(result) + return +} + +// List takes label and field selectors, and returns the list of GitHubIdentityProviders that match those selectors. +func (c *gitHubIdentityProviders) List(ctx context.Context, opts v1.ListOptions) (result *v1alpha1.GitHubIdentityProviderList, err error) { + var timeout time.Duration + if opts.TimeoutSeconds != nil { + timeout = time.Duration(*opts.TimeoutSeconds) * time.Second + } + result = &v1alpha1.GitHubIdentityProviderList{} + err = c.client.Get(). + Namespace(c.ns). + Resource("githubidentityproviders"). + VersionedParams(&opts, scheme.ParameterCodec). + Timeout(timeout). + Do(ctx). + Into(result) + return +} + +// Watch returns a watch.Interface that watches the requested gitHubIdentityProviders. +func (c *gitHubIdentityProviders) Watch(ctx context.Context, opts v1.ListOptions) (watch.Interface, error) { + var timeout time.Duration + if opts.TimeoutSeconds != nil { + timeout = time.Duration(*opts.TimeoutSeconds) * time.Second + } + opts.Watch = true + return c.client.Get(). + Namespace(c.ns). + Resource("githubidentityproviders"). + VersionedParams(&opts, scheme.ParameterCodec). + Timeout(timeout). + Watch(ctx) +} + +// Create takes the representation of a gitHubIdentityProvider and creates it. Returns the server's representation of the gitHubIdentityProvider, and an error, if there is any. +func (c *gitHubIdentityProviders) Create(ctx context.Context, gitHubIdentityProvider *v1alpha1.GitHubIdentityProvider, opts v1.CreateOptions) (result *v1alpha1.GitHubIdentityProvider, err error) { + result = &v1alpha1.GitHubIdentityProvider{} + err = c.client.Post(). + Namespace(c.ns). + Resource("githubidentityproviders"). + VersionedParams(&opts, scheme.ParameterCodec). + Body(gitHubIdentityProvider). + Do(ctx). + Into(result) + return +} + +// Update takes the representation of a gitHubIdentityProvider and updates it. Returns the server's representation of the gitHubIdentityProvider, and an error, if there is any. +func (c *gitHubIdentityProviders) Update(ctx context.Context, gitHubIdentityProvider *v1alpha1.GitHubIdentityProvider, opts v1.UpdateOptions) (result *v1alpha1.GitHubIdentityProvider, err error) { + result = &v1alpha1.GitHubIdentityProvider{} + err = c.client.Put(). + Namespace(c.ns). + Resource("githubidentityproviders"). + Name(gitHubIdentityProvider.Name). + VersionedParams(&opts, scheme.ParameterCodec). + Body(gitHubIdentityProvider). + Do(ctx). + Into(result) + return +} + +// UpdateStatus was generated because the type contains a Status member. +// Add a +genclient:noStatus comment above the type to avoid generating UpdateStatus(). +func (c *gitHubIdentityProviders) UpdateStatus(ctx context.Context, gitHubIdentityProvider *v1alpha1.GitHubIdentityProvider, opts v1.UpdateOptions) (result *v1alpha1.GitHubIdentityProvider, err error) { + result = &v1alpha1.GitHubIdentityProvider{} + err = c.client.Put(). + Namespace(c.ns). + Resource("githubidentityproviders"). + Name(gitHubIdentityProvider.Name). + SubResource("status"). + VersionedParams(&opts, scheme.ParameterCodec). + Body(gitHubIdentityProvider). + Do(ctx). + Into(result) + return +} + +// Delete takes name of the gitHubIdentityProvider and deletes it. Returns an error if one occurs. +func (c *gitHubIdentityProviders) Delete(ctx context.Context, name string, opts v1.DeleteOptions) error { + return c.client.Delete(). + Namespace(c.ns). + Resource("githubidentityproviders"). + Name(name). + Body(&opts). + Do(ctx). + Error() +} + +// DeleteCollection deletes a collection of objects. +func (c *gitHubIdentityProviders) DeleteCollection(ctx context.Context, opts v1.DeleteOptions, listOpts v1.ListOptions) error { + var timeout time.Duration + if listOpts.TimeoutSeconds != nil { + timeout = time.Duration(*listOpts.TimeoutSeconds) * time.Second + } + return c.client.Delete(). + Namespace(c.ns). + Resource("githubidentityproviders"). + VersionedParams(&listOpts, scheme.ParameterCodec). + Timeout(timeout). + Body(&opts). + Do(ctx). + Error() +} + +// Patch applies the patch and returns the patched gitHubIdentityProvider. +func (c *gitHubIdentityProviders) Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts v1.PatchOptions, subresources ...string) (result *v1alpha1.GitHubIdentityProvider, err error) { + result = &v1alpha1.GitHubIdentityProvider{} + err = c.client.Patch(pt). + Namespace(c.ns). + Resource("githubidentityproviders"). + Name(name). + SubResource(subresources...). + VersionedParams(&opts, scheme.ParameterCodec). + Body(data). + Do(ctx). + Into(result) + return +} diff --git a/generated/1.29/client/supervisor/clientset/versioned/typed/idp/v1alpha1/idp_client.go b/generated/1.29/client/supervisor/clientset/versioned/typed/idp/v1alpha1/idp_client.go index 1146d0dc1..f84063db4 100644 --- a/generated/1.29/client/supervisor/clientset/versioned/typed/idp/v1alpha1/idp_client.go +++ b/generated/1.29/client/supervisor/clientset/versioned/typed/idp/v1alpha1/idp_client.go @@ -16,6 +16,7 @@ import ( type IDPV1alpha1Interface interface { RESTClient() rest.Interface ActiveDirectoryIdentityProvidersGetter + GitHubIdentityProvidersGetter LDAPIdentityProvidersGetter OIDCIdentityProvidersGetter } @@ -29,6 +30,10 @@ func (c *IDPV1alpha1Client) ActiveDirectoryIdentityProviders(namespace string) A return newActiveDirectoryIdentityProviders(c, namespace) } +func (c *IDPV1alpha1Client) GitHubIdentityProviders(namespace string) GitHubIdentityProviderInterface { + return newGitHubIdentityProviders(c, namespace) +} + func (c *IDPV1alpha1Client) LDAPIdentityProviders(namespace string) LDAPIdentityProviderInterface { return newLDAPIdentityProviders(c, namespace) } diff --git a/generated/1.29/client/supervisor/informers/externalversions/generic.go b/generated/1.29/client/supervisor/informers/externalversions/generic.go index 7f6c3edfe..eaa915525 100644 --- a/generated/1.29/client/supervisor/informers/externalversions/generic.go +++ b/generated/1.29/client/supervisor/informers/externalversions/generic.go @@ -49,6 +49,8 @@ func (f *sharedInformerFactory) ForResource(resource schema.GroupVersionResource // Group=idp.supervisor.pinniped.dev, Version=v1alpha1 case idpv1alpha1.SchemeGroupVersion.WithResource("activedirectoryidentityproviders"): return &genericInformer{resource: resource.GroupResource(), informer: f.IDP().V1alpha1().ActiveDirectoryIdentityProviders().Informer()}, nil + case idpv1alpha1.SchemeGroupVersion.WithResource("githubidentityproviders"): + return &genericInformer{resource: resource.GroupResource(), informer: f.IDP().V1alpha1().GitHubIdentityProviders().Informer()}, nil case idpv1alpha1.SchemeGroupVersion.WithResource("ldapidentityproviders"): return &genericInformer{resource: resource.GroupResource(), informer: f.IDP().V1alpha1().LDAPIdentityProviders().Informer()}, nil case idpv1alpha1.SchemeGroupVersion.WithResource("oidcidentityproviders"): diff --git a/generated/1.29/client/supervisor/informers/externalversions/idp/v1alpha1/githubidentityprovider.go b/generated/1.29/client/supervisor/informers/externalversions/idp/v1alpha1/githubidentityprovider.go new file mode 100644 index 000000000..b0b233421 --- /dev/null +++ b/generated/1.29/client/supervisor/informers/externalversions/idp/v1alpha1/githubidentityprovider.go @@ -0,0 +1,77 @@ +// Copyright 2020-2024 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +// Code generated by informer-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + "context" + time "time" + + idpv1alpha1 "go.pinniped.dev/generated/1.29/apis/supervisor/idp/v1alpha1" + versioned "go.pinniped.dev/generated/1.29/client/supervisor/clientset/versioned" + internalinterfaces "go.pinniped.dev/generated/1.29/client/supervisor/informers/externalversions/internalinterfaces" + v1alpha1 "go.pinniped.dev/generated/1.29/client/supervisor/listers/idp/v1alpha1" + 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" +) + +// GitHubIdentityProviderInformer provides access to a shared informer and lister for +// GitHubIdentityProviders. +type GitHubIdentityProviderInformer interface { + Informer() cache.SharedIndexInformer + Lister() v1alpha1.GitHubIdentityProviderLister +} + +type gitHubIdentityProviderInformer struct { + factory internalinterfaces.SharedInformerFactory + tweakListOptions internalinterfaces.TweakListOptionsFunc + namespace string +} + +// NewGitHubIdentityProviderInformer constructs a new informer for GitHubIdentityProvider 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 NewGitHubIdentityProviderInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers) cache.SharedIndexInformer { + return NewFilteredGitHubIdentityProviderInformer(client, namespace, resyncPeriod, indexers, nil) +} + +// NewFilteredGitHubIdentityProviderInformer constructs a new informer for GitHubIdentityProvider 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 NewFilteredGitHubIdentityProviderInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers, tweakListOptions internalinterfaces.TweakListOptionsFunc) cache.SharedIndexInformer { + return cache.NewSharedIndexInformer( + &cache.ListWatch{ + ListFunc: func(options v1.ListOptions) (runtime.Object, error) { + if tweakListOptions != nil { + tweakListOptions(&options) + } + return client.IDPV1alpha1().GitHubIdentityProviders(namespace).List(context.TODO(), options) + }, + WatchFunc: func(options v1.ListOptions) (watch.Interface, error) { + if tweakListOptions != nil { + tweakListOptions(&options) + } + return client.IDPV1alpha1().GitHubIdentityProviders(namespace).Watch(context.TODO(), options) + }, + }, + &idpv1alpha1.GitHubIdentityProvider{}, + resyncPeriod, + indexers, + ) +} + +func (f *gitHubIdentityProviderInformer) defaultInformer(client versioned.Interface, resyncPeriod time.Duration) cache.SharedIndexInformer { + return NewFilteredGitHubIdentityProviderInformer(client, f.namespace, resyncPeriod, cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc}, f.tweakListOptions) +} + +func (f *gitHubIdentityProviderInformer) Informer() cache.SharedIndexInformer { + return f.factory.InformerFor(&idpv1alpha1.GitHubIdentityProvider{}, f.defaultInformer) +} + +func (f *gitHubIdentityProviderInformer) Lister() v1alpha1.GitHubIdentityProviderLister { + return v1alpha1.NewGitHubIdentityProviderLister(f.Informer().GetIndexer()) +} diff --git a/generated/1.29/client/supervisor/informers/externalversions/idp/v1alpha1/interface.go b/generated/1.29/client/supervisor/informers/externalversions/idp/v1alpha1/interface.go index 8e6ee3f4b..39eece260 100644 --- a/generated/1.29/client/supervisor/informers/externalversions/idp/v1alpha1/interface.go +++ b/generated/1.29/client/supervisor/informers/externalversions/idp/v1alpha1/interface.go @@ -13,6 +13,8 @@ import ( type Interface interface { // ActiveDirectoryIdentityProviders returns a ActiveDirectoryIdentityProviderInformer. ActiveDirectoryIdentityProviders() ActiveDirectoryIdentityProviderInformer + // GitHubIdentityProviders returns a GitHubIdentityProviderInformer. + GitHubIdentityProviders() GitHubIdentityProviderInformer // LDAPIdentityProviders returns a LDAPIdentityProviderInformer. LDAPIdentityProviders() LDAPIdentityProviderInformer // OIDCIdentityProviders returns a OIDCIdentityProviderInformer. @@ -35,6 +37,11 @@ func (v *version) ActiveDirectoryIdentityProviders() ActiveDirectoryIdentityProv return &activeDirectoryIdentityProviderInformer{factory: v.factory, namespace: v.namespace, tweakListOptions: v.tweakListOptions} } +// GitHubIdentityProviders returns a GitHubIdentityProviderInformer. +func (v *version) GitHubIdentityProviders() GitHubIdentityProviderInformer { + return &gitHubIdentityProviderInformer{factory: v.factory, namespace: v.namespace, tweakListOptions: v.tweakListOptions} +} + // LDAPIdentityProviders returns a LDAPIdentityProviderInformer. func (v *version) LDAPIdentityProviders() LDAPIdentityProviderInformer { return &lDAPIdentityProviderInformer{factory: v.factory, namespace: v.namespace, tweakListOptions: v.tweakListOptions} diff --git a/generated/1.29/client/supervisor/listers/idp/v1alpha1/expansion_generated.go b/generated/1.29/client/supervisor/listers/idp/v1alpha1/expansion_generated.go index b491a9e8d..470c06abb 100644 --- a/generated/1.29/client/supervisor/listers/idp/v1alpha1/expansion_generated.go +++ b/generated/1.29/client/supervisor/listers/idp/v1alpha1/expansion_generated.go @@ -13,6 +13,14 @@ type ActiveDirectoryIdentityProviderListerExpansion interface{} // ActiveDirectoryIdentityProviderNamespaceLister. type ActiveDirectoryIdentityProviderNamespaceListerExpansion interface{} +// GitHubIdentityProviderListerExpansion allows custom methods to be added to +// GitHubIdentityProviderLister. +type GitHubIdentityProviderListerExpansion interface{} + +// GitHubIdentityProviderNamespaceListerExpansion allows custom methods to be added to +// GitHubIdentityProviderNamespaceLister. +type GitHubIdentityProviderNamespaceListerExpansion interface{} + // LDAPIdentityProviderListerExpansion allows custom methods to be added to // LDAPIdentityProviderLister. type LDAPIdentityProviderListerExpansion interface{} diff --git a/generated/1.29/client/supervisor/listers/idp/v1alpha1/githubidentityprovider.go b/generated/1.29/client/supervisor/listers/idp/v1alpha1/githubidentityprovider.go new file mode 100644 index 000000000..63a6fcec7 --- /dev/null +++ b/generated/1.29/client/supervisor/listers/idp/v1alpha1/githubidentityprovider.go @@ -0,0 +1,86 @@ +// Copyright 2020-2024 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +// Code generated by lister-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + v1alpha1 "go.pinniped.dev/generated/1.29/apis/supervisor/idp/v1alpha1" + "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/client-go/tools/cache" +) + +// GitHubIdentityProviderLister helps list GitHubIdentityProviders. +// All objects returned here must be treated as read-only. +type GitHubIdentityProviderLister interface { + // List lists all GitHubIdentityProviders in the indexer. + // Objects returned here must be treated as read-only. + List(selector labels.Selector) (ret []*v1alpha1.GitHubIdentityProvider, err error) + // GitHubIdentityProviders returns an object that can list and get GitHubIdentityProviders. + GitHubIdentityProviders(namespace string) GitHubIdentityProviderNamespaceLister + GitHubIdentityProviderListerExpansion +} + +// gitHubIdentityProviderLister implements the GitHubIdentityProviderLister interface. +type gitHubIdentityProviderLister struct { + indexer cache.Indexer +} + +// NewGitHubIdentityProviderLister returns a new GitHubIdentityProviderLister. +func NewGitHubIdentityProviderLister(indexer cache.Indexer) GitHubIdentityProviderLister { + return &gitHubIdentityProviderLister{indexer: indexer} +} + +// List lists all GitHubIdentityProviders in the indexer. +func (s *gitHubIdentityProviderLister) List(selector labels.Selector) (ret []*v1alpha1.GitHubIdentityProvider, err error) { + err = cache.ListAll(s.indexer, selector, func(m interface{}) { + ret = append(ret, m.(*v1alpha1.GitHubIdentityProvider)) + }) + return ret, err +} + +// GitHubIdentityProviders returns an object that can list and get GitHubIdentityProviders. +func (s *gitHubIdentityProviderLister) GitHubIdentityProviders(namespace string) GitHubIdentityProviderNamespaceLister { + return gitHubIdentityProviderNamespaceLister{indexer: s.indexer, namespace: namespace} +} + +// GitHubIdentityProviderNamespaceLister helps list and get GitHubIdentityProviders. +// All objects returned here must be treated as read-only. +type GitHubIdentityProviderNamespaceLister interface { + // List lists all GitHubIdentityProviders in the indexer for a given namespace. + // Objects returned here must be treated as read-only. + List(selector labels.Selector) (ret []*v1alpha1.GitHubIdentityProvider, err error) + // Get retrieves the GitHubIdentityProvider from the indexer for a given namespace and name. + // Objects returned here must be treated as read-only. + Get(name string) (*v1alpha1.GitHubIdentityProvider, error) + GitHubIdentityProviderNamespaceListerExpansion +} + +// gitHubIdentityProviderNamespaceLister implements the GitHubIdentityProviderNamespaceLister +// interface. +type gitHubIdentityProviderNamespaceLister struct { + indexer cache.Indexer + namespace string +} + +// List lists all GitHubIdentityProviders in the indexer for a given namespace. +func (s gitHubIdentityProviderNamespaceLister) List(selector labels.Selector) (ret []*v1alpha1.GitHubIdentityProvider, err error) { + err = cache.ListAllByNamespace(s.indexer, s.namespace, selector, func(m interface{}) { + ret = append(ret, m.(*v1alpha1.GitHubIdentityProvider)) + }) + return ret, err +} + +// Get retrieves the GitHubIdentityProvider from the indexer for a given namespace and name. +func (s gitHubIdentityProviderNamespaceLister) Get(name string) (*v1alpha1.GitHubIdentityProvider, error) { + obj, exists, err := s.indexer.GetByKey(s.namespace + "/" + name) + if err != nil { + return nil, err + } + if !exists { + return nil, errors.NewNotFound(v1alpha1.Resource("githubidentityprovider"), name) + } + return obj.(*v1alpha1.GitHubIdentityProvider), nil +} diff --git a/generated/1.29/crds/authentication.concierge.pinniped.dev_jwtauthenticators.yaml b/generated/1.29/crds/authentication.concierge.pinniped.dev_jwtauthenticators.yaml index b464cfa2b..d59fcb783 100644 --- a/generated/1.29/crds/authentication.concierge.pinniped.dev_jwtauthenticators.yaml +++ b/generated/1.29/crds/authentication.concierge.pinniped.dev_jwtauthenticators.yaml @@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - controller-gen.kubebuilder.io/version: v0.14.0 + controller-gen.kubebuilder.io/version: v0.15.0 name: jwtauthenticators.authentication.concierge.pinniped.dev spec: group: authentication.concierge.pinniped.dev diff --git a/generated/1.29/crds/authentication.concierge.pinniped.dev_webhookauthenticators.yaml b/generated/1.29/crds/authentication.concierge.pinniped.dev_webhookauthenticators.yaml index e8cbc6790..4ccd53770 100644 --- a/generated/1.29/crds/authentication.concierge.pinniped.dev_webhookauthenticators.yaml +++ b/generated/1.29/crds/authentication.concierge.pinniped.dev_webhookauthenticators.yaml @@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - controller-gen.kubebuilder.io/version: v0.14.0 + controller-gen.kubebuilder.io/version: v0.15.0 name: webhookauthenticators.authentication.concierge.pinniped.dev spec: group: authentication.concierge.pinniped.dev diff --git a/generated/1.29/crds/config.concierge.pinniped.dev_credentialissuers.yaml b/generated/1.29/crds/config.concierge.pinniped.dev_credentialissuers.yaml index 8d77a6665..26fed7376 100644 --- a/generated/1.29/crds/config.concierge.pinniped.dev_credentialissuers.yaml +++ b/generated/1.29/crds/config.concierge.pinniped.dev_credentialissuers.yaml @@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - controller-gen.kubebuilder.io/version: v0.14.0 + controller-gen.kubebuilder.io/version: v0.15.0 name: credentialissuers.config.concierge.pinniped.dev spec: group: config.concierge.pinniped.dev diff --git a/generated/1.29/crds/config.supervisor.pinniped.dev_federationdomains.yaml b/generated/1.29/crds/config.supervisor.pinniped.dev_federationdomains.yaml index 7bf231443..d43c7406d 100644 --- a/generated/1.29/crds/config.supervisor.pinniped.dev_federationdomains.yaml +++ b/generated/1.29/crds/config.supervisor.pinniped.dev_federationdomains.yaml @@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - controller-gen.kubebuilder.io/version: v0.14.0 + controller-gen.kubebuilder.io/version: v0.15.0 name: federationdomains.config.supervisor.pinniped.dev spec: group: config.supervisor.pinniped.dev diff --git a/generated/1.29/crds/config.supervisor.pinniped.dev_oidcclients.yaml b/generated/1.29/crds/config.supervisor.pinniped.dev_oidcclients.yaml index d33b31bf1..c44ecbf1e 100644 --- a/generated/1.29/crds/config.supervisor.pinniped.dev_oidcclients.yaml +++ b/generated/1.29/crds/config.supervisor.pinniped.dev_oidcclients.yaml @@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - controller-gen.kubebuilder.io/version: v0.14.0 + controller-gen.kubebuilder.io/version: v0.15.0 name: oidcclients.config.supervisor.pinniped.dev spec: group: config.supervisor.pinniped.dev diff --git a/generated/1.29/crds/idp.supervisor.pinniped.dev_activedirectoryidentityproviders.yaml b/generated/1.29/crds/idp.supervisor.pinniped.dev_activedirectoryidentityproviders.yaml index 414ac4f51..062251102 100644 --- a/generated/1.29/crds/idp.supervisor.pinniped.dev_activedirectoryidentityproviders.yaml +++ b/generated/1.29/crds/idp.supervisor.pinniped.dev_activedirectoryidentityproviders.yaml @@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - controller-gen.kubebuilder.io/version: v0.14.0 + controller-gen.kubebuilder.io/version: v0.15.0 name: activedirectoryidentityproviders.idp.supervisor.pinniped.dev spec: group: idp.supervisor.pinniped.dev diff --git a/generated/1.29/crds/idp.supervisor.pinniped.dev_githubidentityproviders.yaml b/generated/1.29/crds/idp.supervisor.pinniped.dev_githubidentityproviders.yaml new file mode 100644 index 000000000..f93108700 --- /dev/null +++ b/generated/1.29/crds/idp.supervisor.pinniped.dev_githubidentityproviders.yaml @@ -0,0 +1,326 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.15.0 + name: githubidentityproviders.idp.supervisor.pinniped.dev +spec: + group: idp.supervisor.pinniped.dev + names: + categories: + - pinniped + - pinniped-idp + - pinniped-idps + kind: GitHubIdentityProvider + listKind: GitHubIdentityProviderList + plural: githubidentityproviders + singular: githubidentityprovider + scope: Namespaced + versions: + - additionalPrinterColumns: + - jsonPath: .spec.githubAPI.host + name: Host + type: string + - jsonPath: .status.phase + name: Status + type: string + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1alpha1 + schema: + openAPIV3Schema: + description: |- + GitHubIdentityProvider describes the configuration of an upstream GitHub identity provider. + This upstream provider can be configured with either a GitHub App or a GitHub OAuth2 App. + + + Right now, only web-based logins are supported, for both the pinniped-cli client and clients configured + as OIDCClients. + 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: + description: Spec for configuring the identity provider. + properties: + allowAuthentication: + description: AllowAuthentication allows customization of who can authenticate + using this IDP and how. + properties: + organizations: + description: Organizations allows customization of which organizations + can authenticate using this IDP. + properties: + allowed: + description: |- + Allowed, when specified, indicates that only users with membership in at least one of the listed + GitHub organizations may log in. In addition, the group membership presented to Kubernetes will only include + teams within the listed GitHub organizations. Additional login rules or group filtering can optionally be + provided as policy expression on any Pinniped Supervisor FederationDomain that includes this IDP. + + + The configured GitHub App or GitHub OAuth App must be allowed to see membership in the listed organizations, + otherwise Pinniped will not be aware that the user belongs to the listed organization or any teams + within that organization. + + + If no organizations are listed, you must set organizations: AllGitHubUsers. + items: + type: string + maxItems: 64 + type: array + x-kubernetes-list-type: set + policy: + default: OnlyUsersFromAllowedOrganizations + description: |- + Policy must be set to "AllGitHubUsers" if allowed is empty. + + + This field only exists to ensure that Pinniped administrators are aware that an empty list of + allowedOrganizations means all GitHub users are allowed to log in. + enum: + - OnlyUsersFromAllowedOrganizations + - AllGitHubUsers + type: string + type: object + x-kubernetes-validations: + - message: spec.allowAuthentication.organizations.policy must + be 'OnlyUsersFromAllowedOrganizations' when spec.allowAuthentication.organizations.allowed + has organizations listed + rule: '!(has(self.allowed) && size(self.allowed) > 0 && self.policy + == ''AllGitHubUsers'')' + - message: spec.allowAuthentication.organizations.policy must + be 'AllGitHubUsers' when spec.allowAuthentication.organizations.allowed + is empty + rule: '!((!has(self.allowed) || size(self.allowed) == 0) && + self.policy == ''OnlyUsersFromAllowedOrganizations'')' + required: + - organizations + type: object + claims: + default: {} + description: Claims allows customization of the username and groups + claims. + properties: + groups: + default: slug + description: |- + Groups configures which property of the GitHub team record shall determine the group names in Kubernetes. + + + Can be either "name" or "slug". Defaults to "slug". + + + GitHub team names can contain upper and lower case characters, whitespace, and punctuation (e.g. "Kube admins!"). + + + GitHub team slugs are lower case alphanumeric characters and may contain dashes and underscores (e.g. "kube-admins"). + + + Group names as presented to Kubernetes will always be prefixed by the GitHub organization name followed by a + forward slash (e.g. "my-org/my-team"). GitHub organization login names can only contain alphanumeric characters + or single hyphens, so the first forward slash `/` will be the separator between the organization login name and + the team name or slug. + + + If desired, an admin could configure identity transformation expressions on the Pinniped Supervisor's + FederationDomain to further customize how these group names are presented to Kubernetes. + + + See the response schema for + [List teams for the authenticated user](https://docs.github.com/en/rest/teams/teams?apiVersion=2022-11-28#list-teams-for-the-authenticated-user). + enum: + - name + - slug + type: string + username: + default: login:id + description: |- + Username configures which property of the GitHub user record shall determine the username in Kubernetes. + + + Can be either "id", "login", or "login:id". Defaults to "login:id". + + + GitHub's user login attributes can only contain alphanumeric characters and non-repeating hyphens, + and may not start or end with hyphens. GitHub users are allowed to change their login name, + although it is inconvenient. If a GitHub user changed their login name from "foo" to "bar", + then a second user might change their name from "baz" to "foo" in order to take the old + username of the first user. For this reason, it is not as safe to make authorization decisions + based only on the user's login attribute. + + + If desired, an admin could configure identity transformation expressions on the Pinniped Supervisor's + FederationDomain to further customize how these usernames are presented to Kubernetes. + + + Defaults to "login:id", which is the user login attribute, followed by a colon, followed by the unique and + unchanging integer ID number attribute. This blends human-readable login names with the unchanging ID value + from GitHub. Colons are not allowed in GitHub login attributes or ID numbers, so this is a reasonable + choice to concatenate the two values. + + + See the response schema for + [Get the authenticated user](https://docs.github.com/en/rest/users/users?apiVersion=2022-11-28#get-the-authenticated-user). + enum: + - id + - login + - login:id + type: string + type: object + client: + description: Client identifies the secret with credentials for a GitHub + App or GitHub OAuth2 App (a GitHub client). + properties: + secretName: + description: |- + SecretName contains the name of a namespace-local Secret object that provides the clientID and + clientSecret for an GitHub App or GitHub OAuth2 client. + + + This secret must be of type "secrets.pinniped.dev/github-client" with keys "clientID" and "clientSecret". + minLength: 1 + type: string + required: + - secretName + type: object + githubAPI: + default: {} + description: GitHubAPI allows configuration for GitHub Enterprise + Server + properties: + host: + default: github.com + description: |- + Host is required only for GitHub Enterprise Server. + Defaults to using GitHub's public API ("github.com"). + Do not specify a protocol or scheme since "https://" will always be used. + Port is optional. Do not specify a path, query, fragment, or userinfo. + Only domain name or IP address, subdomains (optional), and port (optional). + IPv4 and IPv6 are supported. If using an IPv6 address with a port, you must enclose the IPv6 address + in square brackets. Example: "[::1]:443". + minLength: 1 + type: string + tls: + description: TLS configuration for GitHub Enterprise Server. + properties: + certificateAuthorityData: + description: X.509 Certificate Authority (base64-encoded PEM + bundle). If omitted, a default set of system roots will + be trusted. + type: string + type: object + type: object + required: + - allowAuthentication + - client + type: object + status: + description: Status of the identity provider. + properties: + conditions: + description: Conditions represents the observations of an identity + provider's current state. + items: + description: "Condition contains details for one aspect of the current + state of this API Resource.\n---\nThis struct is intended for + direct use as an array at the field path .status.conditions. For + example,\n\n\n\ttype FooStatus struct{\n\t // Represents the + observations of a foo's current state.\n\t // Known .status.conditions.type + are: \"Available\", \"Progressing\", and \"Degraded\"\n\t // + +patchMergeKey=type\n\t // +patchStrategy=merge\n\t // +listType=map\n\t + \ // +listMapKey=type\n\t Conditions []metav1.Condition `json:\"conditions,omitempty\" + patchStrategy:\"merge\" patchMergeKey:\"type\" protobuf:\"bytes,1,rep,name=conditions\"`\n\n\n\t + \ // other fields\n\t}" + properties: + lastTransitionTime: + description: |- + lastTransitionTime is the last time the condition transitioned from one status to another. + This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: |- + message is a human readable message indicating details about the transition. + This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: |- + observedGeneration represents the .metadata.generation that the condition was set based upon. + For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date + with respect to the current state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: |- + reason contains a programmatic identifier indicating the reason for the condition's last transition. + Producers of specific condition types may define expected values and meanings for this field, + and whether the values are considered a guaranteed API. + The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: |- + type of condition in CamelCase or in foo.example.com/CamelCase. + --- + Many .condition.type values are consistent across resources like Available, but because arbitrary conditions can be + useful (see .node.status.conditions), the ability to deconflict is important. + The regex it matches is (dns1123SubdomainFmt/)?(qualifiedNameFmt) + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + x-kubernetes-list-map-keys: + - type + x-kubernetes-list-type: map + phase: + default: Pending + description: Phase summarizes the overall status of the GitHubIdentityProvider. + enum: + - Pending + - Ready + - Error + type: string + type: object + required: + - spec + type: object + served: true + storage: true + subresources: + status: {} diff --git a/generated/1.29/crds/idp.supervisor.pinniped.dev_ldapidentityproviders.yaml b/generated/1.29/crds/idp.supervisor.pinniped.dev_ldapidentityproviders.yaml index f718e93aa..711e9a754 100644 --- a/generated/1.29/crds/idp.supervisor.pinniped.dev_ldapidentityproviders.yaml +++ b/generated/1.29/crds/idp.supervisor.pinniped.dev_ldapidentityproviders.yaml @@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - controller-gen.kubebuilder.io/version: v0.14.0 + controller-gen.kubebuilder.io/version: v0.15.0 name: ldapidentityproviders.idp.supervisor.pinniped.dev spec: group: idp.supervisor.pinniped.dev diff --git a/generated/1.29/crds/idp.supervisor.pinniped.dev_oidcidentityproviders.yaml b/generated/1.29/crds/idp.supervisor.pinniped.dev_oidcidentityproviders.yaml index 486665605..acfca1573 100644 --- a/generated/1.29/crds/idp.supervisor.pinniped.dev_oidcidentityproviders.yaml +++ b/generated/1.29/crds/idp.supervisor.pinniped.dev_oidcidentityproviders.yaml @@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - controller-gen.kubebuilder.io/version: v0.14.0 + controller-gen.kubebuilder.io/version: v0.15.0 name: oidcidentityproviders.idp.supervisor.pinniped.dev spec: group: idp.supervisor.pinniped.dev diff --git a/generated/1.30/README.adoc b/generated/1.30/README.adoc index 3b9dd2991..337aacd2a 100644 --- a/generated/1.30/README.adoc +++ b/generated/1.30/README.adoc @@ -1645,6 +1645,285 @@ Optional, when empty this defaults to "objectGUID". + |=== +[id="{anchor_prefix}-go-pinniped-dev-generated-1-30-apis-supervisor-idp-v1alpha1-githubapiconfig"] +==== GitHubAPIConfig + +GitHubAPIConfig allows configuration for GitHub Enterprise Server + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-30-apis-supervisor-idp-v1alpha1-githubidentityproviderspec[$$GitHubIdentityProviderSpec$$] +**** + +[cols="25a,75a", options="header"] +|=== +| Field | Description +| *`host`* __string__ | Host is required only for GitHub Enterprise Server. + +Defaults to using GitHub's public API ("github.com"). + +Do not specify a protocol or scheme since "https://" will always be used. + +Port is optional. Do not specify a path, query, fragment, or userinfo. + +Only domain name or IP address, subdomains (optional), and port (optional). + +IPv4 and IPv6 are supported. If using an IPv6 address with a port, you must enclose the IPv6 address + +in square brackets. Example: "[::1]:443". + +| *`tls`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-30-apis-supervisor-idp-v1alpha1-tlsspec[$$TLSSpec$$]__ | TLS configuration for GitHub Enterprise Server. + +|=== + + +[id="{anchor_prefix}-go-pinniped-dev-generated-1-30-apis-supervisor-idp-v1alpha1-githuballowauthenticationspec"] +==== GitHubAllowAuthenticationSpec + +GitHubAllowAuthenticationSpec allows customization of who can authenticate using this IDP and how. + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-30-apis-supervisor-idp-v1alpha1-githubidentityproviderspec[$$GitHubIdentityProviderSpec$$] +**** + +[cols="25a,75a", options="header"] +|=== +| Field | Description +| *`organizations`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-30-apis-supervisor-idp-v1alpha1-githuborganizationsspec[$$GitHubOrganizationsSpec$$]__ | Organizations allows customization of which organizations can authenticate using this IDP. + +|=== + + +[id="{anchor_prefix}-go-pinniped-dev-generated-1-30-apis-supervisor-idp-v1alpha1-githuballowedauthorganizationspolicy"] +==== GitHubAllowedAuthOrganizationsPolicy (string) + + + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-30-apis-supervisor-idp-v1alpha1-githuborganizationsspec[$$GitHubOrganizationsSpec$$] +**** + + + +[id="{anchor_prefix}-go-pinniped-dev-generated-1-30-apis-supervisor-idp-v1alpha1-githubclaims"] +==== GitHubClaims + +GitHubClaims allows customization of the username and groups claims. + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-30-apis-supervisor-idp-v1alpha1-githubidentityproviderspec[$$GitHubIdentityProviderSpec$$] +**** + +[cols="25a,75a", options="header"] +|=== +| Field | Description +| *`username`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-30-apis-supervisor-idp-v1alpha1-githubusernameattribute[$$GitHubUsernameAttribute$$]__ | Username configures which property of the GitHub user record shall determine the username in Kubernetes. + + + +Can be either "id", "login", or "login:id". Defaults to "login:id". + + + +GitHub's user login attributes can only contain alphanumeric characters and non-repeating hyphens, + +and may not start or end with hyphens. GitHub users are allowed to change their login name, + +although it is inconvenient. If a GitHub user changed their login name from "foo" to "bar", + +then a second user might change their name from "baz" to "foo" in order to take the old + +username of the first user. For this reason, it is not as safe to make authorization decisions + +based only on the user's login attribute. + + + +If desired, an admin could configure identity transformation expressions on the Pinniped Supervisor's + +FederationDomain to further customize how these usernames are presented to Kubernetes. + + + +Defaults to "login:id", which is the user login attribute, followed by a colon, followed by the unique and + +unchanging integer ID number attribute. This blends human-readable login names with the unchanging ID value + +from GitHub. Colons are not allowed in GitHub login attributes or ID numbers, so this is a reasonable + +choice to concatenate the two values. + + + +See the response schema for + +[Get the authenticated user](https://docs.github.com/en/rest/users/users?apiVersion=2022-11-28#get-the-authenticated-user). + +| *`groups`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-30-apis-supervisor-idp-v1alpha1-githubgroupnameattribute[$$GitHubGroupNameAttribute$$]__ | Groups configures which property of the GitHub team record shall determine the group names in Kubernetes. + + + +Can be either "name" or "slug". Defaults to "slug". + + + +GitHub team names can contain upper and lower case characters, whitespace, and punctuation (e.g. "Kube admins!"). + + + +GitHub team slugs are lower case alphanumeric characters and may contain dashes and underscores (e.g. "kube-admins"). + + + +Group names as presented to Kubernetes will always be prefixed by the GitHub organization name followed by a + +forward slash (e.g. "my-org/my-team"). GitHub organization login names can only contain alphanumeric characters + +or single hyphens, so the first forward slash `/` will be the separator between the organization login name and + +the team name or slug. + + + +If desired, an admin could configure identity transformation expressions on the Pinniped Supervisor's + +FederationDomain to further customize how these group names are presented to Kubernetes. + + + +See the response schema for + +[List teams for the authenticated user](https://docs.github.com/en/rest/teams/teams?apiVersion=2022-11-28#list-teams-for-the-authenticated-user). + +|=== + + +[id="{anchor_prefix}-go-pinniped-dev-generated-1-30-apis-supervisor-idp-v1alpha1-githubclientspec"] +==== GitHubClientSpec + +GitHubClientSpec contains information about the GitHub client that this identity provider will use +for web-based login flows. + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-30-apis-supervisor-idp-v1alpha1-githubidentityproviderspec[$$GitHubIdentityProviderSpec$$] +**** + +[cols="25a,75a", options="header"] +|=== +| Field | Description +| *`secretName`* __string__ | SecretName contains the name of a namespace-local Secret object that provides the clientID and + +clientSecret for an GitHub App or GitHub OAuth2 client. + + + +This secret must be of type "secrets.pinniped.dev/github-client" with keys "clientID" and "clientSecret". + +|=== + + +[id="{anchor_prefix}-go-pinniped-dev-generated-1-30-apis-supervisor-idp-v1alpha1-githubgroupnameattribute"] +==== GitHubGroupNameAttribute (string) + +GitHubGroupNameAttribute allows the user to specify which attribute from GitHub to use for the group +names to present to Kubernetes. See the response schema for +[List teams for the authenticated user](https://docs.github.com/en/rest/teams/teams?apiVersion=2022-11-28#list-teams-for-the-authenticated-user). + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-30-apis-supervisor-idp-v1alpha1-githubclaims[$$GitHubClaims$$] +**** + + + +[id="{anchor_prefix}-go-pinniped-dev-generated-1-30-apis-supervisor-idp-v1alpha1-githubidentityprovider"] +==== GitHubIdentityProvider + +GitHubIdentityProvider describes the configuration of an upstream GitHub identity provider. +This upstream provider can be configured with either a GitHub App or a GitHub OAuth2 App. + + +Right now, only web-based logins are supported, for both the pinniped-cli client and clients configured +as OIDCClients. + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-30-apis-supervisor-idp-v1alpha1-githubidentityproviderlist[$$GitHubIdentityProviderList$$] +**** + +[cols="25a,75a", options="header"] +|=== +| Field | Description +| *`metadata`* __link:https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.3/#objectmeta-v1-meta[$$ObjectMeta$$]__ | Refer to Kubernetes API documentation for fields of `metadata`. + +| *`spec`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-30-apis-supervisor-idp-v1alpha1-githubidentityproviderspec[$$GitHubIdentityProviderSpec$$]__ | Spec for configuring the identity provider. + +| *`status`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-30-apis-supervisor-idp-v1alpha1-githubidentityproviderstatus[$$GitHubIdentityProviderStatus$$]__ | Status of the identity provider. + +|=== + + + + +[id="{anchor_prefix}-go-pinniped-dev-generated-1-30-apis-supervisor-idp-v1alpha1-githubidentityproviderphase"] +==== GitHubIdentityProviderPhase (string) + + + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-30-apis-supervisor-idp-v1alpha1-githubidentityproviderstatus[$$GitHubIdentityProviderStatus$$] +**** + + + +[id="{anchor_prefix}-go-pinniped-dev-generated-1-30-apis-supervisor-idp-v1alpha1-githubidentityproviderspec"] +==== GitHubIdentityProviderSpec + +GitHubIdentityProviderSpec is the spec for configuring an GitHub identity provider. + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-30-apis-supervisor-idp-v1alpha1-githubidentityprovider[$$GitHubIdentityProvider$$] +**** + +[cols="25a,75a", options="header"] +|=== +| Field | Description +| *`githubAPI`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-30-apis-supervisor-idp-v1alpha1-githubapiconfig[$$GitHubAPIConfig$$]__ | GitHubAPI allows configuration for GitHub Enterprise Server + +| *`claims`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-30-apis-supervisor-idp-v1alpha1-githubclaims[$$GitHubClaims$$]__ | Claims allows customization of the username and groups claims. + +| *`allowAuthentication`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-30-apis-supervisor-idp-v1alpha1-githuballowauthenticationspec[$$GitHubAllowAuthenticationSpec$$]__ | AllowAuthentication allows customization of who can authenticate using this IDP and how. + +| *`client`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-30-apis-supervisor-idp-v1alpha1-githubclientspec[$$GitHubClientSpec$$]__ | Client identifies the secret with credentials for a GitHub App or GitHub OAuth2 App (a GitHub client). + +|=== + + +[id="{anchor_prefix}-go-pinniped-dev-generated-1-30-apis-supervisor-idp-v1alpha1-githubidentityproviderstatus"] +==== GitHubIdentityProviderStatus + +GitHubIdentityProviderStatus is the status of an GitHub identity provider. + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-30-apis-supervisor-idp-v1alpha1-githubidentityprovider[$$GitHubIdentityProvider$$] +**** + +[cols="25a,75a", options="header"] +|=== +| Field | Description +| *`phase`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-30-apis-supervisor-idp-v1alpha1-githubidentityproviderphase[$$GitHubIdentityProviderPhase$$]__ | Phase summarizes the overall status of the GitHubIdentityProvider. + +| *`conditions`* __link:https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.3/#condition-v1-meta[$$Condition$$] array__ | Conditions represents the observations of an identity provider's current state. + +|=== + + +[id="{anchor_prefix}-go-pinniped-dev-generated-1-30-apis-supervisor-idp-v1alpha1-githuborganizationsspec"] +==== GitHubOrganizationsSpec + + + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-30-apis-supervisor-idp-v1alpha1-githuballowauthenticationspec[$$GitHubAllowAuthenticationSpec$$] +**** + +[cols="25a,75a", options="header"] +|=== +| Field | Description +| *`policy`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-30-apis-supervisor-idp-v1alpha1-githuballowedauthorganizationspolicy[$$GitHubAllowedAuthOrganizationsPolicy$$]__ | Policy must be set to "AllGitHubUsers" if allowed is empty. + + + +This field only exists to ensure that Pinniped administrators are aware that an empty list of + +allowedOrganizations means all GitHub users are allowed to log in. + +| *`allowed`* __string array__ | Allowed, when specified, indicates that only users with membership in at least one of the listed + +GitHub organizations may log in. In addition, the group membership presented to Kubernetes will only include + +teams within the listed GitHub organizations. Additional login rules or group filtering can optionally be + +provided as policy expression on any Pinniped Supervisor FederationDomain that includes this IDP. + + + +The configured GitHub App or GitHub OAuth App must be allowed to see membership in the listed organizations, + +otherwise Pinniped will not be aware that the user belongs to the listed organization or any teams + +within that organization. + + + +If no organizations are listed, you must set organizations: AllGitHubUsers. + +|=== + + +[id="{anchor_prefix}-go-pinniped-dev-generated-1-30-apis-supervisor-idp-v1alpha1-githubusernameattribute"] +==== GitHubUsernameAttribute (string) + +GitHubUsernameAttribute allows the user to specify which attribute(s) from GitHub to use for the username to present +to Kubernetes. See the response schema for +[Get the authenticated user](https://docs.github.com/en/rest/users/users?apiVersion=2022-11-28#get-the-authenticated-user). + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-30-apis-supervisor-idp-v1alpha1-githubclaims[$$GitHubClaims$$] +**** + + + [id="{anchor_prefix}-go-pinniped-dev-generated-1-30-apis-supervisor-idp-v1alpha1-ldapidentityprovider"] ==== LDAPIdentityProvider @@ -2108,11 +2387,12 @@ Parameter is a key/value pair which represents a parameter in an HTTP request. [id="{anchor_prefix}-go-pinniped-dev-generated-1-30-apis-supervisor-idp-v1alpha1-tlsspec"] ==== TLSSpec -Configuration for TLS parameters related to identity provider integration. +TLSSpec provides TLS configuration for identity provider integration. .Appears In: **** - xref:{anchor_prefix}-go-pinniped-dev-generated-1-30-apis-supervisor-idp-v1alpha1-activedirectoryidentityproviderspec[$$ActiveDirectoryIdentityProviderSpec$$] +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-30-apis-supervisor-idp-v1alpha1-githubapiconfig[$$GitHubAPIConfig$$] - xref:{anchor_prefix}-go-pinniped-dev-generated-1-30-apis-supervisor-idp-v1alpha1-ldapidentityproviderspec[$$LDAPIdentityProviderSpec$$] - xref:{anchor_prefix}-go-pinniped-dev-generated-1-30-apis-supervisor-idp-v1alpha1-oidcidentityproviderspec[$$OIDCIdentityProviderSpec$$] **** diff --git a/generated/1.30/apis/go.mod b/generated/1.30/apis/go.mod index 83b94a46c..2dcc40ab0 100644 --- a/generated/1.30/apis/go.mod +++ b/generated/1.30/apis/go.mod @@ -3,11 +3,11 @@ module go.pinniped.dev/generated/1.30/apis go 1.22.0 -toolchain go1.22.3 +toolchain go1.22.4 require ( - k8s.io/api v0.30.0 - k8s.io/apimachinery v0.30.0 + k8s.io/api v0.30.1 + k8s.io/apimachinery v0.30.1 ) require ( diff --git a/generated/1.30/apis/go.sum b/generated/1.30/apis/go.sum index 57dd389b7..0e725e65c 100644 --- a/generated/1.30/apis/go.sum +++ b/generated/1.30/apis/go.sum @@ -75,10 +75,10 @@ gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -k8s.io/api v0.30.0 h1:siWhRq7cNjy2iHssOB9SCGNCl2spiF1dO3dABqZ8niA= -k8s.io/api v0.30.0/go.mod h1:OPlaYhoHs8EQ1ql0R/TsUgaRPhpKNxIMrKQfWUp8QSE= -k8s.io/apimachinery v0.30.0 h1:qxVPsyDM5XS96NIh9Oj6LavoVFYff/Pon9cZeDIkHHA= -k8s.io/apimachinery v0.30.0/go.mod h1:iexa2somDaxdnj7bha06bhb43Zpa6eWH8N8dbqVjTUc= +k8s.io/api v0.30.1 h1:kCm/6mADMdbAxmIh0LBjS54nQBE+U4KmbCfIkF5CpJY= +k8s.io/api v0.30.1/go.mod h1:ddbN2C0+0DIiPntan/bye3SW3PdwLa11/0yqwvuRrJM= +k8s.io/apimachinery v0.30.1 h1:ZQStsEfo4n65yAdlGTfP/uSHMQSoYzU/oeEbkmF7P2U= +k8s.io/apimachinery v0.30.1/go.mod h1:iexa2somDaxdnj7bha06bhb43Zpa6eWH8N8dbqVjTUc= k8s.io/klog/v2 v2.120.1 h1:QXU6cPEOIslTGvZaXvFWiP9VKyeet3sawzTOvdXb4Vw= k8s.io/klog/v2 v2.120.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= k8s.io/utils v0.0.0-20230726121419-3b25d923346b h1:sgn3ZU783SCgtaSJjpcVVlRqd6GSnlTLKgpAAttJvpI= diff --git a/generated/1.30/apis/supervisor/idp/v1alpha1/register.go b/generated/1.30/apis/supervisor/idp/v1alpha1/register.go index 8829a8638..705be8076 100644 --- a/generated/1.30/apis/supervisor/idp/v1alpha1/register.go +++ b/generated/1.30/apis/supervisor/idp/v1alpha1/register.go @@ -1,4 +1,4 @@ -// Copyright 2020-2022 the Pinniped contributors. All Rights Reserved. +// Copyright 2020-2024 the Pinniped contributors. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 package v1alpha1 @@ -36,6 +36,8 @@ func addKnownTypes(scheme *runtime.Scheme) error { &LDAPIdentityProviderList{}, &ActiveDirectoryIdentityProvider{}, &ActiveDirectoryIdentityProviderList{}, + &GitHubIdentityProvider{}, + &GitHubIdentityProviderList{}, ) metav1.AddToGroupVersion(scheme, SchemeGroupVersion) return nil diff --git a/generated/1.30/apis/supervisor/idp/v1alpha1/types_githubidentityprovider.go b/generated/1.30/apis/supervisor/idp/v1alpha1/types_githubidentityprovider.go new file mode 100644 index 000000000..c84f46dbd --- /dev/null +++ b/generated/1.30/apis/supervisor/idp/v1alpha1/types_githubidentityprovider.go @@ -0,0 +1,256 @@ +// Copyright 2024 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package v1alpha1 + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +type GitHubIdentityProviderPhase string + +const ( + // GitHubPhasePending is the default phase for newly-created GitHubIdentityProvider resources. + GitHubPhasePending GitHubIdentityProviderPhase = "Pending" + + // GitHubPhaseReady is the phase for an GitHubIdentityProvider resource in a healthy state. + GitHubPhaseReady GitHubIdentityProviderPhase = "Ready" + + // GitHubPhaseError is the phase for an GitHubIdentityProvider in an unhealthy state. + GitHubPhaseError GitHubIdentityProviderPhase = "Error" +) + +type GitHubAllowedAuthOrganizationsPolicy string + +const ( + // GitHubAllowedAuthOrganizationsPolicyAllGitHubUsers means any GitHub user is allowed to log in using this identity + // provider, regardless of their organization membership or lack thereof. + GitHubAllowedAuthOrganizationsPolicyAllGitHubUsers GitHubAllowedAuthOrganizationsPolicy = "AllGitHubUsers" + + // GitHubAllowedAuthOrganizationsPolicyOnlyUsersFromAllowedOrganizations means only those users with membership in + // the listed GitHub organizations are allowed to log in. + GitHubAllowedAuthOrganizationsPolicyOnlyUsersFromAllowedOrganizations GitHubAllowedAuthOrganizationsPolicy = "OnlyUsersFromAllowedOrganizations" +) + +// GitHubIdentityProviderStatus is the status of an GitHub identity provider. +type GitHubIdentityProviderStatus struct { + // Phase summarizes the overall status of the GitHubIdentityProvider. + // + // +kubebuilder:default=Pending + // +kubebuilder:validation:Enum=Pending;Ready;Error + Phase GitHubIdentityProviderPhase `json:"phase,omitempty"` + + // Conditions represents the observations of an identity provider's current state. + // + // +patchMergeKey=type + // +patchStrategy=merge + // +listType=map + // +listMapKey=type + Conditions []metav1.Condition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type"` +} + +// GitHubAPIConfig allows configuration for GitHub Enterprise Server +type GitHubAPIConfig struct { + // Host is required only for GitHub Enterprise Server. + // Defaults to using GitHub's public API ("github.com"). + // Do not specify a protocol or scheme since "https://" will always be used. + // Port is optional. Do not specify a path, query, fragment, or userinfo. + // Only domain name or IP address, subdomains (optional), and port (optional). + // IPv4 and IPv6 are supported. If using an IPv6 address with a port, you must enclose the IPv6 address + // in square brackets. Example: "[::1]:443". + // + // +kubebuilder:default="github.com" + // +kubebuilder:validation:MinLength=1 + // +optional + Host *string `json:"host"` + + // TLS configuration for GitHub Enterprise Server. + // + // +optional + TLS *TLSSpec `json:"tls,omitempty"` +} + +// GitHubUsernameAttribute allows the user to specify which attribute(s) from GitHub to use for the username to present +// to Kubernetes. See the response schema for +// [Get the authenticated user](https://docs.github.com/en/rest/users/users?apiVersion=2022-11-28#get-the-authenticated-user). +type GitHubUsernameAttribute string + +const ( + // GitHubUsernameID specifies using the `id` attribute from the GitHub user for the username to present to Kubernetes. + GitHubUsernameID GitHubUsernameAttribute = "id" + + // GitHubUsernameLogin specifies using the `login` attribute from the GitHub user as the username to present to Kubernetes. + GitHubUsernameLogin GitHubUsernameAttribute = "login" + + // GitHubUsernameLoginAndID specifies combining the `login` and `id` attributes from the GitHub user as the + // username to present to Kubernetes, separated by a colon. Example: "my-login:1234" + GitHubUsernameLoginAndID GitHubUsernameAttribute = "login:id" +) + +// GitHubGroupNameAttribute allows the user to specify which attribute from GitHub to use for the group +// names to present to Kubernetes. See the response schema for +// [List teams for the authenticated user](https://docs.github.com/en/rest/teams/teams?apiVersion=2022-11-28#list-teams-for-the-authenticated-user). +type GitHubGroupNameAttribute string + +const ( + // GitHubUseTeamNameForGroupName specifies using the GitHub team's `name` attribute as the group name to present to Kubernetes. + GitHubUseTeamNameForGroupName GitHubGroupNameAttribute = "name" + + // GitHubUseTeamSlugForGroupName specifies using the GitHub team's `slug` attribute as the group name to present to Kubernetes. + GitHubUseTeamSlugForGroupName GitHubGroupNameAttribute = "slug" +) + +// GitHubClaims allows customization of the username and groups claims. +type GitHubClaims struct { + // Username configures which property of the GitHub user record shall determine the username in Kubernetes. + // + // Can be either "id", "login", or "login:id". Defaults to "login:id". + // + // GitHub's user login attributes can only contain alphanumeric characters and non-repeating hyphens, + // and may not start or end with hyphens. GitHub users are allowed to change their login name, + // although it is inconvenient. If a GitHub user changed their login name from "foo" to "bar", + // then a second user might change their name from "baz" to "foo" in order to take the old + // username of the first user. For this reason, it is not as safe to make authorization decisions + // based only on the user's login attribute. + // + // If desired, an admin could configure identity transformation expressions on the Pinniped Supervisor's + // FederationDomain to further customize how these usernames are presented to Kubernetes. + // + // Defaults to "login:id", which is the user login attribute, followed by a colon, followed by the unique and + // unchanging integer ID number attribute. This blends human-readable login names with the unchanging ID value + // from GitHub. Colons are not allowed in GitHub login attributes or ID numbers, so this is a reasonable + // choice to concatenate the two values. + // + // See the response schema for + // [Get the authenticated user](https://docs.github.com/en/rest/users/users?apiVersion=2022-11-28#get-the-authenticated-user). + // + // +kubebuilder:default="login:id" + // +kubebuilder:validation:Enum={"id","login","login:id"} + // +optional + Username *GitHubUsernameAttribute `json:"username"` + + // Groups configures which property of the GitHub team record shall determine the group names in Kubernetes. + // + // Can be either "name" or "slug". Defaults to "slug". + // + // GitHub team names can contain upper and lower case characters, whitespace, and punctuation (e.g. "Kube admins!"). + // + // GitHub team slugs are lower case alphanumeric characters and may contain dashes and underscores (e.g. "kube-admins"). + // + // Group names as presented to Kubernetes will always be prefixed by the GitHub organization name followed by a + // forward slash (e.g. "my-org/my-team"). GitHub organization login names can only contain alphanumeric characters + // or single hyphens, so the first forward slash `/` will be the separator between the organization login name and + // the team name or slug. + // + // If desired, an admin could configure identity transformation expressions on the Pinniped Supervisor's + // FederationDomain to further customize how these group names are presented to Kubernetes. + // + // See the response schema for + // [List teams for the authenticated user](https://docs.github.com/en/rest/teams/teams?apiVersion=2022-11-28#list-teams-for-the-authenticated-user). + // + // +kubebuilder:default=slug + // +kubebuilder:validation:Enum=name;slug + // +optional + Groups *GitHubGroupNameAttribute `json:"groups"` +} + +// GitHubClientSpec contains information about the GitHub client that this identity provider will use +// for web-based login flows. +type GitHubClientSpec struct { + // SecretName contains the name of a namespace-local Secret object that provides the clientID and + // clientSecret for an GitHub App or GitHub OAuth2 client. + // + // This secret must be of type "secrets.pinniped.dev/github-client" with keys "clientID" and "clientSecret". + // + // +kubebuilder:validation:MinLength=1 + SecretName string `json:"secretName"` +} + +type GitHubOrganizationsSpec struct { + // Policy must be set to "AllGitHubUsers" if allowed is empty. + // + // This field only exists to ensure that Pinniped administrators are aware that an empty list of + // allowedOrganizations means all GitHub users are allowed to log in. + // + // +kubebuilder:default=OnlyUsersFromAllowedOrganizations + // +kubebuilder:validation:Enum=OnlyUsersFromAllowedOrganizations;AllGitHubUsers + // +optional + Policy *GitHubAllowedAuthOrganizationsPolicy `json:"policy"` + + // Allowed, when specified, indicates that only users with membership in at least one of the listed + // GitHub organizations may log in. In addition, the group membership presented to Kubernetes will only include + // teams within the listed GitHub organizations. Additional login rules or group filtering can optionally be + // provided as policy expression on any Pinniped Supervisor FederationDomain that includes this IDP. + // + // The configured GitHub App or GitHub OAuth App must be allowed to see membership in the listed organizations, + // otherwise Pinniped will not be aware that the user belongs to the listed organization or any teams + // within that organization. + // + // If no organizations are listed, you must set organizations: AllGitHubUsers. + // + // +kubebuilder:validation:MaxItems=64 + // +listType=set + // +optional + Allowed []string `json:"allowed,omitempty"` +} + +// GitHubAllowAuthenticationSpec allows customization of who can authenticate using this IDP and how. +type GitHubAllowAuthenticationSpec struct { + // Organizations allows customization of which organizations can authenticate using this IDP. + // +kubebuilder:validation:XValidation:message="spec.allowAuthentication.organizations.policy must be 'OnlyUsersFromAllowedOrganizations' when spec.allowAuthentication.organizations.allowed has organizations listed",rule="!(has(self.allowed) && size(self.allowed) > 0 && self.policy == 'AllGitHubUsers')" + // +kubebuilder:validation:XValidation:message="spec.allowAuthentication.organizations.policy must be 'AllGitHubUsers' when spec.allowAuthentication.organizations.allowed is empty",rule="!((!has(self.allowed) || size(self.allowed) == 0) && self.policy == 'OnlyUsersFromAllowedOrganizations')" + Organizations GitHubOrganizationsSpec `json:"organizations"` +} + +// GitHubIdentityProviderSpec is the spec for configuring an GitHub identity provider. +type GitHubIdentityProviderSpec struct { + // GitHubAPI allows configuration for GitHub Enterprise Server + // + // +kubebuilder:default={} + GitHubAPI GitHubAPIConfig `json:"githubAPI,omitempty"` + + // Claims allows customization of the username and groups claims. + // + // +kubebuilder:default={} + Claims GitHubClaims `json:"claims,omitempty"` + + // AllowAuthentication allows customization of who can authenticate using this IDP and how. + AllowAuthentication GitHubAllowAuthenticationSpec `json:"allowAuthentication"` + + // Client identifies the secret with credentials for a GitHub App or GitHub OAuth2 App (a GitHub client). + Client GitHubClientSpec `json:"client"` +} + +// GitHubIdentityProvider describes the configuration of an upstream GitHub identity provider. +// This upstream provider can be configured with either a GitHub App or a GitHub OAuth2 App. +// +// Right now, only web-based logins are supported, for both the pinniped-cli client and clients configured +// as OIDCClients. +// +// +genclient +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object +// +kubebuilder:resource:categories=pinniped;pinniped-idp;pinniped-idps +// +kubebuilder:printcolumn:name="Host",type=string,JSONPath=`.spec.githubAPI.host` +// +kubebuilder:printcolumn:name="Status",type=string,JSONPath=`.status.phase` +// +kubebuilder:printcolumn:name="Age",type=date,JSONPath=`.metadata.creationTimestamp` +// +kubebuilder:subresource:status +type GitHubIdentityProvider struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + // Spec for configuring the identity provider. + Spec GitHubIdentityProviderSpec `json:"spec"` + + // Status of the identity provider. + Status GitHubIdentityProviderStatus `json:"status,omitempty"` +} + +// GitHubIdentityProviderList lists GitHubIdentityProvider objects. +// +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object +type GitHubIdentityProviderList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + + Items []GitHubIdentityProvider `json:"items"` +} diff --git a/generated/1.30/apis/supervisor/idp/v1alpha1/types_tls.go b/generated/1.30/apis/supervisor/idp/v1alpha1/types_tls.go index 1413a262c..49b49373c 100644 --- a/generated/1.30/apis/supervisor/idp/v1alpha1/types_tls.go +++ b/generated/1.30/apis/supervisor/idp/v1alpha1/types_tls.go @@ -1,9 +1,9 @@ -// Copyright 2020-2022 the Pinniped contributors. All Rights Reserved. +// Copyright 2020-2024 the Pinniped contributors. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 package v1alpha1 -// Configuration for TLS parameters related to identity provider integration. +// TLSSpec provides TLS configuration for identity provider integration. type TLSSpec struct { // X.509 Certificate Authority (base64-encoded PEM bundle). If omitted, a default set of system roots will be trusted. // +optional diff --git a/generated/1.30/apis/supervisor/idp/v1alpha1/zz_generated.deepcopy.go b/generated/1.30/apis/supervisor/idp/v1alpha1/zz_generated.deepcopy.go index b0cb168b5..e48860e82 100644 --- a/generated/1.30/apis/supervisor/idp/v1alpha1/zz_generated.deepcopy.go +++ b/generated/1.30/apis/supervisor/idp/v1alpha1/zz_generated.deepcopy.go @@ -203,6 +203,221 @@ func (in *ActiveDirectoryIdentityProviderUserSearchAttributes) DeepCopy() *Activ return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *GitHubAPIConfig) DeepCopyInto(out *GitHubAPIConfig) { + *out = *in + if in.Host != nil { + in, out := &in.Host, &out.Host + *out = new(string) + **out = **in + } + if in.TLS != nil { + in, out := &in.TLS, &out.TLS + *out = new(TLSSpec) + **out = **in + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GitHubAPIConfig. +func (in *GitHubAPIConfig) DeepCopy() *GitHubAPIConfig { + if in == nil { + return nil + } + out := new(GitHubAPIConfig) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *GitHubAllowAuthenticationSpec) DeepCopyInto(out *GitHubAllowAuthenticationSpec) { + *out = *in + in.Organizations.DeepCopyInto(&out.Organizations) + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GitHubAllowAuthenticationSpec. +func (in *GitHubAllowAuthenticationSpec) DeepCopy() *GitHubAllowAuthenticationSpec { + if in == nil { + return nil + } + out := new(GitHubAllowAuthenticationSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *GitHubClaims) DeepCopyInto(out *GitHubClaims) { + *out = *in + if in.Username != nil { + in, out := &in.Username, &out.Username + *out = new(GitHubUsernameAttribute) + **out = **in + } + if in.Groups != nil { + in, out := &in.Groups, &out.Groups + *out = new(GitHubGroupNameAttribute) + **out = **in + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GitHubClaims. +func (in *GitHubClaims) DeepCopy() *GitHubClaims { + if in == nil { + return nil + } + out := new(GitHubClaims) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *GitHubClientSpec) DeepCopyInto(out *GitHubClientSpec) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GitHubClientSpec. +func (in *GitHubClientSpec) DeepCopy() *GitHubClientSpec { + if in == nil { + return nil + } + out := new(GitHubClientSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *GitHubIdentityProvider) DeepCopyInto(out *GitHubIdentityProvider) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + in.Status.DeepCopyInto(&out.Status) + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GitHubIdentityProvider. +func (in *GitHubIdentityProvider) DeepCopy() *GitHubIdentityProvider { + if in == nil { + return nil + } + out := new(GitHubIdentityProvider) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *GitHubIdentityProvider) 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 *GitHubIdentityProviderList) DeepCopyInto(out *GitHubIdentityProviderList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]GitHubIdentityProvider, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GitHubIdentityProviderList. +func (in *GitHubIdentityProviderList) DeepCopy() *GitHubIdentityProviderList { + if in == nil { + return nil + } + out := new(GitHubIdentityProviderList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *GitHubIdentityProviderList) 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 *GitHubIdentityProviderSpec) DeepCopyInto(out *GitHubIdentityProviderSpec) { + *out = *in + in.GitHubAPI.DeepCopyInto(&out.GitHubAPI) + in.Claims.DeepCopyInto(&out.Claims) + in.AllowAuthentication.DeepCopyInto(&out.AllowAuthentication) + out.Client = in.Client + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GitHubIdentityProviderSpec. +func (in *GitHubIdentityProviderSpec) DeepCopy() *GitHubIdentityProviderSpec { + if in == nil { + return nil + } + out := new(GitHubIdentityProviderSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *GitHubIdentityProviderStatus) DeepCopyInto(out *GitHubIdentityProviderStatus) { + *out = *in + if in.Conditions != nil { + in, out := &in.Conditions, &out.Conditions + *out = make([]v1.Condition, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GitHubIdentityProviderStatus. +func (in *GitHubIdentityProviderStatus) DeepCopy() *GitHubIdentityProviderStatus { + if in == nil { + return nil + } + out := new(GitHubIdentityProviderStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *GitHubOrganizationsSpec) DeepCopyInto(out *GitHubOrganizationsSpec) { + *out = *in + if in.Policy != nil { + in, out := &in.Policy, &out.Policy + *out = new(GitHubAllowedAuthOrganizationsPolicy) + **out = **in + } + if in.Allowed != nil { + in, out := &in.Allowed, &out.Allowed + *out = make([]string, len(*in)) + copy(*out, *in) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GitHubOrganizationsSpec. +func (in *GitHubOrganizationsSpec) DeepCopy() *GitHubOrganizationsSpec { + if in == nil { + return nil + } + out := new(GitHubOrganizationsSpec) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *LDAPIdentityProvider) DeepCopyInto(out *LDAPIdentityProvider) { *out = *in diff --git a/generated/1.30/apis/supervisor/idpdiscovery/v1alpha1/types_supervisor_idp_discovery.go b/generated/1.30/apis/supervisor/idpdiscovery/v1alpha1/types_supervisor_idp_discovery.go index 837fed2dd..11be6f9dc 100644 --- a/generated/1.30/apis/supervisor/idpdiscovery/v1alpha1/types_supervisor_idp_discovery.go +++ b/generated/1.30/apis/supervisor/idpdiscovery/v1alpha1/types_supervisor_idp_discovery.go @@ -15,6 +15,7 @@ const ( IDPTypeOIDC IDPType = "oidc" IDPTypeLDAP IDPType = "ldap" IDPTypeActiveDirectory IDPType = "activedirectory" + IDPTypeGitHub IDPType = "github" IDPFlowCLIPassword IDPFlow = "cli_password" IDPFlowBrowserAuthcode IDPFlow = "browser_authcode" diff --git a/generated/1.30/client/concierge/openapi/zz_generated.openapi.go b/generated/1.30/client/concierge/openapi/zz_generated.openapi.go index 89d9d8d78..908bf3737 100644 --- a/generated/1.30/client/concierge/openapi/zz_generated.openapi.go +++ b/generated/1.30/client/concierge/openapi/zz_generated.openapi.go @@ -1875,7 +1875,8 @@ func schema_k8sio_api_core_v1_ConfigMapEnvSource(ref common.ReferenceCallback) c Properties: map[string]spec.Schema{ "name": { SchemaProps: spec.SchemaProps{ - Description: "Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names", + Description: "Name of the referent. This field is effectively required, but due to backwards compatibility is allowed to be empty. Instances of this type with an empty value here are almost certainly wrong. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names", + Default: "", Type: []string{"string"}, Format: "", }, @@ -1902,7 +1903,8 @@ func schema_k8sio_api_core_v1_ConfigMapKeySelector(ref common.ReferenceCallback) Properties: map[string]spec.Schema{ "name": { SchemaProps: spec.SchemaProps{ - Description: "Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names", + Description: "Name of the referent. This field is effectively required, but due to backwards compatibility is allowed to be empty. Instances of this type with an empty value here are almost certainly wrong. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names", + Default: "", Type: []string{"string"}, Format: "", }, @@ -2046,7 +2048,8 @@ func schema_k8sio_api_core_v1_ConfigMapProjection(ref common.ReferenceCallback) Properties: map[string]spec.Schema{ "name": { SchemaProps: spec.SchemaProps{ - Description: "Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names", + Description: "Name of the referent. This field is effectively required, but due to backwards compatibility is allowed to be empty. Instances of this type with an empty value here are almost certainly wrong. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names", + Default: "", Type: []string{"string"}, Format: "", }, @@ -2094,7 +2097,8 @@ func schema_k8sio_api_core_v1_ConfigMapVolumeSource(ref common.ReferenceCallback Properties: map[string]spec.Schema{ "name": { SchemaProps: spec.SchemaProps{ - Description: "Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names", + Description: "Name of the referent. This field is effectively required, but due to backwards compatibility is allowed to be empty. Instances of this type with an empty value here are almost certainly wrong. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names", + Default: "", Type: []string{"string"}, Format: "", }, @@ -4804,6 +4808,7 @@ func schema_k8sio_api_core_v1_HostAlias(ref common.ReferenceCallback) common.Ope "ip": { SchemaProps: spec.SchemaProps{ Description: "IP address of the host file entry.", + Default: "", Type: []string{"string"}, Format: "", }, @@ -4829,6 +4834,7 @@ func schema_k8sio_api_core_v1_HostAlias(ref common.ReferenceCallback) common.Ope }, }, }, + Required: []string{"ip"}, }, }, } @@ -5574,7 +5580,8 @@ func schema_k8sio_api_core_v1_LocalObjectReference(ref common.ReferenceCallback) Properties: map[string]spec.Schema{ "name": { SchemaProps: spec.SchemaProps{ - Description: "Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names", + Description: "Name of the referent. This field is effectively required, but due to backwards compatibility is allowed to be empty. Instances of this type with an empty value here are almost certainly wrong. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names", + Default: "", Type: []string{"string"}, Format: "", }, @@ -11527,7 +11534,8 @@ func schema_k8sio_api_core_v1_SecretEnvSource(ref common.ReferenceCallback) comm Properties: map[string]spec.Schema{ "name": { SchemaProps: spec.SchemaProps{ - Description: "Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names", + Description: "Name of the referent. This field is effectively required, but due to backwards compatibility is allowed to be empty. Instances of this type with an empty value here are almost certainly wrong. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names", + Default: "", Type: []string{"string"}, Format: "", }, @@ -11554,7 +11562,8 @@ func schema_k8sio_api_core_v1_SecretKeySelector(ref common.ReferenceCallback) co Properties: map[string]spec.Schema{ "name": { SchemaProps: spec.SchemaProps{ - Description: "Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names", + Description: "Name of the referent. This field is effectively required, but due to backwards compatibility is allowed to be empty. Instances of this type with an empty value here are almost certainly wrong. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names", + Default: "", Type: []string{"string"}, Format: "", }, @@ -11646,7 +11655,8 @@ func schema_k8sio_api_core_v1_SecretProjection(ref common.ReferenceCallback) com Properties: map[string]spec.Schema{ "name": { SchemaProps: spec.SchemaProps{ - Description: "Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names", + Description: "Name of the referent. This field is effectively required, but due to backwards compatibility is allowed to be empty. Instances of this type with an empty value here are almost certainly wrong. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names", + Default: "", Type: []string{"string"}, Format: "", }, @@ -12502,7 +12512,7 @@ func schema_k8sio_api_core_v1_ServiceSpec(ref common.ReferenceCallback) common.O }, "trafficDistribution": { SchemaProps: spec.SchemaProps{ - Description: "TrafficDistribution offers a way to express preferences for how traffic is distributed to Service endpoints. Implementations can use this field as a hint, but are not required to guarantee strict adherence. If the field is not set, the implementation will apply its default routing strategy. If set to \"PreferClose\", implementations should prioritize endpoints that are topologically close (e.g., same zone).", + Description: "TrafficDistribution offers a way to express preferences for how traffic is distributed to Service endpoints. Implementations can use this field as a hint, but are not required to guarantee strict adherence. If the field is not set, the implementation will apply its default routing strategy. If set to \"PreferClose\", implementations should prioritize endpoints that are topologically close (e.g., same zone). This is an alpha field and requires enabling ServiceTrafficDistribution feature.", Type: []string{"string"}, Format: "", }, diff --git a/generated/1.30/client/go.mod b/generated/1.30/client/go.mod index 5b7c50a2e..5db1a675c 100644 --- a/generated/1.30/client/go.mod +++ b/generated/1.30/client/go.mod @@ -3,14 +3,14 @@ module go.pinniped.dev/generated/1.30/client go 1.22.0 -toolchain go1.22.3 +toolchain go1.22.4 replace go.pinniped.dev/generated/1.30/apis => ../apis require ( go.pinniped.dev/generated/1.30/apis v0.0.0 - k8s.io/apimachinery v0.30.0 - k8s.io/client-go v0.30.0 + k8s.io/apimachinery v0.30.1 + k8s.io/client-go v0.30.1 k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340 ) @@ -46,7 +46,7 @@ require ( gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect - k8s.io/api v0.30.0 // indirect + k8s.io/api v0.30.1 // indirect k8s.io/klog/v2 v2.120.1 // indirect k8s.io/utils v0.0.0-20230726121419-3b25d923346b // indirect sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect diff --git a/generated/1.30/client/go.sum b/generated/1.30/client/go.sum index 6a10d0e17..c138aa8d9 100644 --- a/generated/1.30/client/go.sum +++ b/generated/1.30/client/go.sum @@ -134,12 +134,12 @@ gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -k8s.io/api v0.30.0 h1:siWhRq7cNjy2iHssOB9SCGNCl2spiF1dO3dABqZ8niA= -k8s.io/api v0.30.0/go.mod h1:OPlaYhoHs8EQ1ql0R/TsUgaRPhpKNxIMrKQfWUp8QSE= -k8s.io/apimachinery v0.30.0 h1:qxVPsyDM5XS96NIh9Oj6LavoVFYff/Pon9cZeDIkHHA= -k8s.io/apimachinery v0.30.0/go.mod h1:iexa2somDaxdnj7bha06bhb43Zpa6eWH8N8dbqVjTUc= -k8s.io/client-go v0.30.0 h1:sB1AGGlhY/o7KCyCEQ0bPWzYDL0pwOZO4vAtTSh/gJQ= -k8s.io/client-go v0.30.0/go.mod h1:g7li5O5256qe6TYdAMyX/otJqMhIiGgTapdLchhmOaY= +k8s.io/api v0.30.1 h1:kCm/6mADMdbAxmIh0LBjS54nQBE+U4KmbCfIkF5CpJY= +k8s.io/api v0.30.1/go.mod h1:ddbN2C0+0DIiPntan/bye3SW3PdwLa11/0yqwvuRrJM= +k8s.io/apimachinery v0.30.1 h1:ZQStsEfo4n65yAdlGTfP/uSHMQSoYzU/oeEbkmF7P2U= +k8s.io/apimachinery v0.30.1/go.mod h1:iexa2somDaxdnj7bha06bhb43Zpa6eWH8N8dbqVjTUc= +k8s.io/client-go v0.30.1 h1:uC/Ir6A3R46wdkgCV3vbLyNOYyCJ8oZnjtJGKfytl/Q= +k8s.io/client-go v0.30.1/go.mod h1:wrAqLNs2trwiCH/wxxmT/x3hKVH9PuV0GGW0oDoHVqc= k8s.io/klog/v2 v2.120.1 h1:QXU6cPEOIslTGvZaXvFWiP9VKyeet3sawzTOvdXb4Vw= k8s.io/klog/v2 v2.120.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340 h1:BZqlfIlq5YbRMFko6/PM7FjZpUb45WallggurYhKGag= diff --git a/generated/1.30/client/supervisor/clientset/versioned/typed/idp/v1alpha1/fake/fake_githubidentityprovider.go b/generated/1.30/client/supervisor/clientset/versioned/typed/idp/v1alpha1/fake/fake_githubidentityprovider.go new file mode 100644 index 000000000..28f206628 --- /dev/null +++ b/generated/1.30/client/supervisor/clientset/versioned/typed/idp/v1alpha1/fake/fake_githubidentityprovider.go @@ -0,0 +1,128 @@ +// Copyright 2020-2024 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +// Code generated by client-gen. DO NOT EDIT. + +package fake + +import ( + "context" + + v1alpha1 "go.pinniped.dev/generated/1.30/apis/supervisor/idp/v1alpha1" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + labels "k8s.io/apimachinery/pkg/labels" + types "k8s.io/apimachinery/pkg/types" + watch "k8s.io/apimachinery/pkg/watch" + testing "k8s.io/client-go/testing" +) + +// FakeGitHubIdentityProviders implements GitHubIdentityProviderInterface +type FakeGitHubIdentityProviders struct { + Fake *FakeIDPV1alpha1 + ns string +} + +var githubidentityprovidersResource = v1alpha1.SchemeGroupVersion.WithResource("githubidentityproviders") + +var githubidentityprovidersKind = v1alpha1.SchemeGroupVersion.WithKind("GitHubIdentityProvider") + +// Get takes name of the gitHubIdentityProvider, and returns the corresponding gitHubIdentityProvider object, and an error if there is any. +func (c *FakeGitHubIdentityProviders) Get(ctx context.Context, name string, options v1.GetOptions) (result *v1alpha1.GitHubIdentityProvider, err error) { + obj, err := c.Fake. + Invokes(testing.NewGetAction(githubidentityprovidersResource, c.ns, name), &v1alpha1.GitHubIdentityProvider{}) + + if obj == nil { + return nil, err + } + return obj.(*v1alpha1.GitHubIdentityProvider), err +} + +// List takes label and field selectors, and returns the list of GitHubIdentityProviders that match those selectors. +func (c *FakeGitHubIdentityProviders) List(ctx context.Context, opts v1.ListOptions) (result *v1alpha1.GitHubIdentityProviderList, err error) { + obj, err := c.Fake. + Invokes(testing.NewListAction(githubidentityprovidersResource, githubidentityprovidersKind, c.ns, opts), &v1alpha1.GitHubIdentityProviderList{}) + + if obj == nil { + return nil, err + } + + label, _, _ := testing.ExtractFromListOptions(opts) + if label == nil { + label = labels.Everything() + } + list := &v1alpha1.GitHubIdentityProviderList{ListMeta: obj.(*v1alpha1.GitHubIdentityProviderList).ListMeta} + for _, item := range obj.(*v1alpha1.GitHubIdentityProviderList).Items { + if label.Matches(labels.Set(item.Labels)) { + list.Items = append(list.Items, item) + } + } + return list, err +} + +// Watch returns a watch.Interface that watches the requested gitHubIdentityProviders. +func (c *FakeGitHubIdentityProviders) Watch(ctx context.Context, opts v1.ListOptions) (watch.Interface, error) { + return c.Fake. + InvokesWatch(testing.NewWatchAction(githubidentityprovidersResource, c.ns, opts)) + +} + +// Create takes the representation of a gitHubIdentityProvider and creates it. Returns the server's representation of the gitHubIdentityProvider, and an error, if there is any. +func (c *FakeGitHubIdentityProviders) Create(ctx context.Context, gitHubIdentityProvider *v1alpha1.GitHubIdentityProvider, opts v1.CreateOptions) (result *v1alpha1.GitHubIdentityProvider, err error) { + obj, err := c.Fake. + Invokes(testing.NewCreateAction(githubidentityprovidersResource, c.ns, gitHubIdentityProvider), &v1alpha1.GitHubIdentityProvider{}) + + if obj == nil { + return nil, err + } + return obj.(*v1alpha1.GitHubIdentityProvider), err +} + +// Update takes the representation of a gitHubIdentityProvider and updates it. Returns the server's representation of the gitHubIdentityProvider, and an error, if there is any. +func (c *FakeGitHubIdentityProviders) Update(ctx context.Context, gitHubIdentityProvider *v1alpha1.GitHubIdentityProvider, opts v1.UpdateOptions) (result *v1alpha1.GitHubIdentityProvider, err error) { + obj, err := c.Fake. + Invokes(testing.NewUpdateAction(githubidentityprovidersResource, c.ns, gitHubIdentityProvider), &v1alpha1.GitHubIdentityProvider{}) + + if obj == nil { + return nil, err + } + return obj.(*v1alpha1.GitHubIdentityProvider), err +} + +// UpdateStatus was generated because the type contains a Status member. +// Add a +genclient:noStatus comment above the type to avoid generating UpdateStatus(). +func (c *FakeGitHubIdentityProviders) UpdateStatus(ctx context.Context, gitHubIdentityProvider *v1alpha1.GitHubIdentityProvider, opts v1.UpdateOptions) (*v1alpha1.GitHubIdentityProvider, error) { + obj, err := c.Fake. + Invokes(testing.NewUpdateSubresourceAction(githubidentityprovidersResource, "status", c.ns, gitHubIdentityProvider), &v1alpha1.GitHubIdentityProvider{}) + + if obj == nil { + return nil, err + } + return obj.(*v1alpha1.GitHubIdentityProvider), err +} + +// Delete takes name of the gitHubIdentityProvider and deletes it. Returns an error if one occurs. +func (c *FakeGitHubIdentityProviders) Delete(ctx context.Context, name string, opts v1.DeleteOptions) error { + _, err := c.Fake. + Invokes(testing.NewDeleteActionWithOptions(githubidentityprovidersResource, c.ns, name, opts), &v1alpha1.GitHubIdentityProvider{}) + + return err +} + +// DeleteCollection deletes a collection of objects. +func (c *FakeGitHubIdentityProviders) DeleteCollection(ctx context.Context, opts v1.DeleteOptions, listOpts v1.ListOptions) error { + action := testing.NewDeleteCollectionAction(githubidentityprovidersResource, c.ns, listOpts) + + _, err := c.Fake.Invokes(action, &v1alpha1.GitHubIdentityProviderList{}) + return err +} + +// Patch applies the patch and returns the patched gitHubIdentityProvider. +func (c *FakeGitHubIdentityProviders) Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts v1.PatchOptions, subresources ...string) (result *v1alpha1.GitHubIdentityProvider, err error) { + obj, err := c.Fake. + Invokes(testing.NewPatchSubresourceAction(githubidentityprovidersResource, c.ns, name, pt, data, subresources...), &v1alpha1.GitHubIdentityProvider{}) + + if obj == nil { + return nil, err + } + return obj.(*v1alpha1.GitHubIdentityProvider), err +} diff --git a/generated/1.30/client/supervisor/clientset/versioned/typed/idp/v1alpha1/fake/fake_idp_client.go b/generated/1.30/client/supervisor/clientset/versioned/typed/idp/v1alpha1/fake/fake_idp_client.go index 4fa78b165..f587367b9 100644 --- a/generated/1.30/client/supervisor/clientset/versioned/typed/idp/v1alpha1/fake/fake_idp_client.go +++ b/generated/1.30/client/supervisor/clientset/versioned/typed/idp/v1alpha1/fake/fake_idp_client.go @@ -19,6 +19,10 @@ func (c *FakeIDPV1alpha1) ActiveDirectoryIdentityProviders(namespace string) v1a return &FakeActiveDirectoryIdentityProviders{c, namespace} } +func (c *FakeIDPV1alpha1) GitHubIdentityProviders(namespace string) v1alpha1.GitHubIdentityProviderInterface { + return &FakeGitHubIdentityProviders{c, namespace} +} + func (c *FakeIDPV1alpha1) LDAPIdentityProviders(namespace string) v1alpha1.LDAPIdentityProviderInterface { return &FakeLDAPIdentityProviders{c, namespace} } diff --git a/generated/1.30/client/supervisor/clientset/versioned/typed/idp/v1alpha1/generated_expansion.go b/generated/1.30/client/supervisor/clientset/versioned/typed/idp/v1alpha1/generated_expansion.go index 79be098d6..a7acc8120 100644 --- a/generated/1.30/client/supervisor/clientset/versioned/typed/idp/v1alpha1/generated_expansion.go +++ b/generated/1.30/client/supervisor/clientset/versioned/typed/idp/v1alpha1/generated_expansion.go @@ -7,6 +7,8 @@ package v1alpha1 type ActiveDirectoryIdentityProviderExpansion interface{} +type GitHubIdentityProviderExpansion interface{} + type LDAPIdentityProviderExpansion interface{} type OIDCIdentityProviderExpansion interface{} diff --git a/generated/1.30/client/supervisor/clientset/versioned/typed/idp/v1alpha1/githubidentityprovider.go b/generated/1.30/client/supervisor/clientset/versioned/typed/idp/v1alpha1/githubidentityprovider.go new file mode 100644 index 000000000..7f678a1cb --- /dev/null +++ b/generated/1.30/client/supervisor/clientset/versioned/typed/idp/v1alpha1/githubidentityprovider.go @@ -0,0 +1,182 @@ +// Copyright 2020-2024 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +// Code generated by client-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + "context" + "time" + + v1alpha1 "go.pinniped.dev/generated/1.30/apis/supervisor/idp/v1alpha1" + scheme "go.pinniped.dev/generated/1.30/client/supervisor/clientset/versioned/scheme" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + types "k8s.io/apimachinery/pkg/types" + watch "k8s.io/apimachinery/pkg/watch" + rest "k8s.io/client-go/rest" +) + +// GitHubIdentityProvidersGetter has a method to return a GitHubIdentityProviderInterface. +// A group's client should implement this interface. +type GitHubIdentityProvidersGetter interface { + GitHubIdentityProviders(namespace string) GitHubIdentityProviderInterface +} + +// GitHubIdentityProviderInterface has methods to work with GitHubIdentityProvider resources. +type GitHubIdentityProviderInterface interface { + Create(ctx context.Context, gitHubIdentityProvider *v1alpha1.GitHubIdentityProvider, opts v1.CreateOptions) (*v1alpha1.GitHubIdentityProvider, error) + Update(ctx context.Context, gitHubIdentityProvider *v1alpha1.GitHubIdentityProvider, opts v1.UpdateOptions) (*v1alpha1.GitHubIdentityProvider, error) + UpdateStatus(ctx context.Context, gitHubIdentityProvider *v1alpha1.GitHubIdentityProvider, opts v1.UpdateOptions) (*v1alpha1.GitHubIdentityProvider, 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) (*v1alpha1.GitHubIdentityProvider, error) + List(ctx context.Context, opts v1.ListOptions) (*v1alpha1.GitHubIdentityProviderList, 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 *v1alpha1.GitHubIdentityProvider, err error) + GitHubIdentityProviderExpansion +} + +// gitHubIdentityProviders implements GitHubIdentityProviderInterface +type gitHubIdentityProviders struct { + client rest.Interface + ns string +} + +// newGitHubIdentityProviders returns a GitHubIdentityProviders +func newGitHubIdentityProviders(c *IDPV1alpha1Client, namespace string) *gitHubIdentityProviders { + return &gitHubIdentityProviders{ + client: c.RESTClient(), + ns: namespace, + } +} + +// Get takes name of the gitHubIdentityProvider, and returns the corresponding gitHubIdentityProvider object, and an error if there is any. +func (c *gitHubIdentityProviders) Get(ctx context.Context, name string, options v1.GetOptions) (result *v1alpha1.GitHubIdentityProvider, err error) { + result = &v1alpha1.GitHubIdentityProvider{} + err = c.client.Get(). + Namespace(c.ns). + Resource("githubidentityproviders"). + Name(name). + VersionedParams(&options, scheme.ParameterCodec). + Do(ctx). + Into(result) + return +} + +// List takes label and field selectors, and returns the list of GitHubIdentityProviders that match those selectors. +func (c *gitHubIdentityProviders) List(ctx context.Context, opts v1.ListOptions) (result *v1alpha1.GitHubIdentityProviderList, err error) { + var timeout time.Duration + if opts.TimeoutSeconds != nil { + timeout = time.Duration(*opts.TimeoutSeconds) * time.Second + } + result = &v1alpha1.GitHubIdentityProviderList{} + err = c.client.Get(). + Namespace(c.ns). + Resource("githubidentityproviders"). + VersionedParams(&opts, scheme.ParameterCodec). + Timeout(timeout). + Do(ctx). + Into(result) + return +} + +// Watch returns a watch.Interface that watches the requested gitHubIdentityProviders. +func (c *gitHubIdentityProviders) Watch(ctx context.Context, opts v1.ListOptions) (watch.Interface, error) { + var timeout time.Duration + if opts.TimeoutSeconds != nil { + timeout = time.Duration(*opts.TimeoutSeconds) * time.Second + } + opts.Watch = true + return c.client.Get(). + Namespace(c.ns). + Resource("githubidentityproviders"). + VersionedParams(&opts, scheme.ParameterCodec). + Timeout(timeout). + Watch(ctx) +} + +// Create takes the representation of a gitHubIdentityProvider and creates it. Returns the server's representation of the gitHubIdentityProvider, and an error, if there is any. +func (c *gitHubIdentityProviders) Create(ctx context.Context, gitHubIdentityProvider *v1alpha1.GitHubIdentityProvider, opts v1.CreateOptions) (result *v1alpha1.GitHubIdentityProvider, err error) { + result = &v1alpha1.GitHubIdentityProvider{} + err = c.client.Post(). + Namespace(c.ns). + Resource("githubidentityproviders"). + VersionedParams(&opts, scheme.ParameterCodec). + Body(gitHubIdentityProvider). + Do(ctx). + Into(result) + return +} + +// Update takes the representation of a gitHubIdentityProvider and updates it. Returns the server's representation of the gitHubIdentityProvider, and an error, if there is any. +func (c *gitHubIdentityProviders) Update(ctx context.Context, gitHubIdentityProvider *v1alpha1.GitHubIdentityProvider, opts v1.UpdateOptions) (result *v1alpha1.GitHubIdentityProvider, err error) { + result = &v1alpha1.GitHubIdentityProvider{} + err = c.client.Put(). + Namespace(c.ns). + Resource("githubidentityproviders"). + Name(gitHubIdentityProvider.Name). + VersionedParams(&opts, scheme.ParameterCodec). + Body(gitHubIdentityProvider). + Do(ctx). + Into(result) + return +} + +// UpdateStatus was generated because the type contains a Status member. +// Add a +genclient:noStatus comment above the type to avoid generating UpdateStatus(). +func (c *gitHubIdentityProviders) UpdateStatus(ctx context.Context, gitHubIdentityProvider *v1alpha1.GitHubIdentityProvider, opts v1.UpdateOptions) (result *v1alpha1.GitHubIdentityProvider, err error) { + result = &v1alpha1.GitHubIdentityProvider{} + err = c.client.Put(). + Namespace(c.ns). + Resource("githubidentityproviders"). + Name(gitHubIdentityProvider.Name). + SubResource("status"). + VersionedParams(&opts, scheme.ParameterCodec). + Body(gitHubIdentityProvider). + Do(ctx). + Into(result) + return +} + +// Delete takes name of the gitHubIdentityProvider and deletes it. Returns an error if one occurs. +func (c *gitHubIdentityProviders) Delete(ctx context.Context, name string, opts v1.DeleteOptions) error { + return c.client.Delete(). + Namespace(c.ns). + Resource("githubidentityproviders"). + Name(name). + Body(&opts). + Do(ctx). + Error() +} + +// DeleteCollection deletes a collection of objects. +func (c *gitHubIdentityProviders) DeleteCollection(ctx context.Context, opts v1.DeleteOptions, listOpts v1.ListOptions) error { + var timeout time.Duration + if listOpts.TimeoutSeconds != nil { + timeout = time.Duration(*listOpts.TimeoutSeconds) * time.Second + } + return c.client.Delete(). + Namespace(c.ns). + Resource("githubidentityproviders"). + VersionedParams(&listOpts, scheme.ParameterCodec). + Timeout(timeout). + Body(&opts). + Do(ctx). + Error() +} + +// Patch applies the patch and returns the patched gitHubIdentityProvider. +func (c *gitHubIdentityProviders) Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts v1.PatchOptions, subresources ...string) (result *v1alpha1.GitHubIdentityProvider, err error) { + result = &v1alpha1.GitHubIdentityProvider{} + err = c.client.Patch(pt). + Namespace(c.ns). + Resource("githubidentityproviders"). + Name(name). + SubResource(subresources...). + VersionedParams(&opts, scheme.ParameterCodec). + Body(data). + Do(ctx). + Into(result) + return +} diff --git a/generated/1.30/client/supervisor/clientset/versioned/typed/idp/v1alpha1/idp_client.go b/generated/1.30/client/supervisor/clientset/versioned/typed/idp/v1alpha1/idp_client.go index fd8b3d3c4..66f5d3707 100644 --- a/generated/1.30/client/supervisor/clientset/versioned/typed/idp/v1alpha1/idp_client.go +++ b/generated/1.30/client/supervisor/clientset/versioned/typed/idp/v1alpha1/idp_client.go @@ -16,6 +16,7 @@ import ( type IDPV1alpha1Interface interface { RESTClient() rest.Interface ActiveDirectoryIdentityProvidersGetter + GitHubIdentityProvidersGetter LDAPIdentityProvidersGetter OIDCIdentityProvidersGetter } @@ -29,6 +30,10 @@ func (c *IDPV1alpha1Client) ActiveDirectoryIdentityProviders(namespace string) A return newActiveDirectoryIdentityProviders(c, namespace) } +func (c *IDPV1alpha1Client) GitHubIdentityProviders(namespace string) GitHubIdentityProviderInterface { + return newGitHubIdentityProviders(c, namespace) +} + func (c *IDPV1alpha1Client) LDAPIdentityProviders(namespace string) LDAPIdentityProviderInterface { return newLDAPIdentityProviders(c, namespace) } diff --git a/generated/1.30/client/supervisor/informers/externalversions/generic.go b/generated/1.30/client/supervisor/informers/externalversions/generic.go index 8c7f755ec..a167fc8e5 100644 --- a/generated/1.30/client/supervisor/informers/externalversions/generic.go +++ b/generated/1.30/client/supervisor/informers/externalversions/generic.go @@ -49,6 +49,8 @@ func (f *sharedInformerFactory) ForResource(resource schema.GroupVersionResource // Group=idp.supervisor.pinniped.dev, Version=v1alpha1 case idpv1alpha1.SchemeGroupVersion.WithResource("activedirectoryidentityproviders"): return &genericInformer{resource: resource.GroupResource(), informer: f.IDP().V1alpha1().ActiveDirectoryIdentityProviders().Informer()}, nil + case idpv1alpha1.SchemeGroupVersion.WithResource("githubidentityproviders"): + return &genericInformer{resource: resource.GroupResource(), informer: f.IDP().V1alpha1().GitHubIdentityProviders().Informer()}, nil case idpv1alpha1.SchemeGroupVersion.WithResource("ldapidentityproviders"): return &genericInformer{resource: resource.GroupResource(), informer: f.IDP().V1alpha1().LDAPIdentityProviders().Informer()}, nil case idpv1alpha1.SchemeGroupVersion.WithResource("oidcidentityproviders"): diff --git a/generated/1.30/client/supervisor/informers/externalversions/idp/v1alpha1/githubidentityprovider.go b/generated/1.30/client/supervisor/informers/externalversions/idp/v1alpha1/githubidentityprovider.go new file mode 100644 index 000000000..0ff0b4e10 --- /dev/null +++ b/generated/1.30/client/supervisor/informers/externalversions/idp/v1alpha1/githubidentityprovider.go @@ -0,0 +1,77 @@ +// Copyright 2020-2024 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +// Code generated by informer-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + "context" + time "time" + + idpv1alpha1 "go.pinniped.dev/generated/1.30/apis/supervisor/idp/v1alpha1" + versioned "go.pinniped.dev/generated/1.30/client/supervisor/clientset/versioned" + internalinterfaces "go.pinniped.dev/generated/1.30/client/supervisor/informers/externalversions/internalinterfaces" + v1alpha1 "go.pinniped.dev/generated/1.30/client/supervisor/listers/idp/v1alpha1" + 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" +) + +// GitHubIdentityProviderInformer provides access to a shared informer and lister for +// GitHubIdentityProviders. +type GitHubIdentityProviderInformer interface { + Informer() cache.SharedIndexInformer + Lister() v1alpha1.GitHubIdentityProviderLister +} + +type gitHubIdentityProviderInformer struct { + factory internalinterfaces.SharedInformerFactory + tweakListOptions internalinterfaces.TweakListOptionsFunc + namespace string +} + +// NewGitHubIdentityProviderInformer constructs a new informer for GitHubIdentityProvider 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 NewGitHubIdentityProviderInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers) cache.SharedIndexInformer { + return NewFilteredGitHubIdentityProviderInformer(client, namespace, resyncPeriod, indexers, nil) +} + +// NewFilteredGitHubIdentityProviderInformer constructs a new informer for GitHubIdentityProvider 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 NewFilteredGitHubIdentityProviderInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers, tweakListOptions internalinterfaces.TweakListOptionsFunc) cache.SharedIndexInformer { + return cache.NewSharedIndexInformer( + &cache.ListWatch{ + ListFunc: func(options v1.ListOptions) (runtime.Object, error) { + if tweakListOptions != nil { + tweakListOptions(&options) + } + return client.IDPV1alpha1().GitHubIdentityProviders(namespace).List(context.TODO(), options) + }, + WatchFunc: func(options v1.ListOptions) (watch.Interface, error) { + if tweakListOptions != nil { + tweakListOptions(&options) + } + return client.IDPV1alpha1().GitHubIdentityProviders(namespace).Watch(context.TODO(), options) + }, + }, + &idpv1alpha1.GitHubIdentityProvider{}, + resyncPeriod, + indexers, + ) +} + +func (f *gitHubIdentityProviderInformer) defaultInformer(client versioned.Interface, resyncPeriod time.Duration) cache.SharedIndexInformer { + return NewFilteredGitHubIdentityProviderInformer(client, f.namespace, resyncPeriod, cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc}, f.tweakListOptions) +} + +func (f *gitHubIdentityProviderInformer) Informer() cache.SharedIndexInformer { + return f.factory.InformerFor(&idpv1alpha1.GitHubIdentityProvider{}, f.defaultInformer) +} + +func (f *gitHubIdentityProviderInformer) Lister() v1alpha1.GitHubIdentityProviderLister { + return v1alpha1.NewGitHubIdentityProviderLister(f.Informer().GetIndexer()) +} diff --git a/generated/1.30/client/supervisor/informers/externalversions/idp/v1alpha1/interface.go b/generated/1.30/client/supervisor/informers/externalversions/idp/v1alpha1/interface.go index 478759104..ed1e4f5f3 100644 --- a/generated/1.30/client/supervisor/informers/externalversions/idp/v1alpha1/interface.go +++ b/generated/1.30/client/supervisor/informers/externalversions/idp/v1alpha1/interface.go @@ -13,6 +13,8 @@ import ( type Interface interface { // ActiveDirectoryIdentityProviders returns a ActiveDirectoryIdentityProviderInformer. ActiveDirectoryIdentityProviders() ActiveDirectoryIdentityProviderInformer + // GitHubIdentityProviders returns a GitHubIdentityProviderInformer. + GitHubIdentityProviders() GitHubIdentityProviderInformer // LDAPIdentityProviders returns a LDAPIdentityProviderInformer. LDAPIdentityProviders() LDAPIdentityProviderInformer // OIDCIdentityProviders returns a OIDCIdentityProviderInformer. @@ -35,6 +37,11 @@ func (v *version) ActiveDirectoryIdentityProviders() ActiveDirectoryIdentityProv return &activeDirectoryIdentityProviderInformer{factory: v.factory, namespace: v.namespace, tweakListOptions: v.tweakListOptions} } +// GitHubIdentityProviders returns a GitHubIdentityProviderInformer. +func (v *version) GitHubIdentityProviders() GitHubIdentityProviderInformer { + return &gitHubIdentityProviderInformer{factory: v.factory, namespace: v.namespace, tweakListOptions: v.tweakListOptions} +} + // LDAPIdentityProviders returns a LDAPIdentityProviderInformer. func (v *version) LDAPIdentityProviders() LDAPIdentityProviderInformer { return &lDAPIdentityProviderInformer{factory: v.factory, namespace: v.namespace, tweakListOptions: v.tweakListOptions} diff --git a/generated/1.30/client/supervisor/listers/idp/v1alpha1/expansion_generated.go b/generated/1.30/client/supervisor/listers/idp/v1alpha1/expansion_generated.go index b491a9e8d..470c06abb 100644 --- a/generated/1.30/client/supervisor/listers/idp/v1alpha1/expansion_generated.go +++ b/generated/1.30/client/supervisor/listers/idp/v1alpha1/expansion_generated.go @@ -13,6 +13,14 @@ type ActiveDirectoryIdentityProviderListerExpansion interface{} // ActiveDirectoryIdentityProviderNamespaceLister. type ActiveDirectoryIdentityProviderNamespaceListerExpansion interface{} +// GitHubIdentityProviderListerExpansion allows custom methods to be added to +// GitHubIdentityProviderLister. +type GitHubIdentityProviderListerExpansion interface{} + +// GitHubIdentityProviderNamespaceListerExpansion allows custom methods to be added to +// GitHubIdentityProviderNamespaceLister. +type GitHubIdentityProviderNamespaceListerExpansion interface{} + // LDAPIdentityProviderListerExpansion allows custom methods to be added to // LDAPIdentityProviderLister. type LDAPIdentityProviderListerExpansion interface{} diff --git a/generated/1.30/client/supervisor/listers/idp/v1alpha1/githubidentityprovider.go b/generated/1.30/client/supervisor/listers/idp/v1alpha1/githubidentityprovider.go new file mode 100644 index 000000000..94ff1e205 --- /dev/null +++ b/generated/1.30/client/supervisor/listers/idp/v1alpha1/githubidentityprovider.go @@ -0,0 +1,86 @@ +// Copyright 2020-2024 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +// Code generated by lister-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + v1alpha1 "go.pinniped.dev/generated/1.30/apis/supervisor/idp/v1alpha1" + "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/client-go/tools/cache" +) + +// GitHubIdentityProviderLister helps list GitHubIdentityProviders. +// All objects returned here must be treated as read-only. +type GitHubIdentityProviderLister interface { + // List lists all GitHubIdentityProviders in the indexer. + // Objects returned here must be treated as read-only. + List(selector labels.Selector) (ret []*v1alpha1.GitHubIdentityProvider, err error) + // GitHubIdentityProviders returns an object that can list and get GitHubIdentityProviders. + GitHubIdentityProviders(namespace string) GitHubIdentityProviderNamespaceLister + GitHubIdentityProviderListerExpansion +} + +// gitHubIdentityProviderLister implements the GitHubIdentityProviderLister interface. +type gitHubIdentityProviderLister struct { + indexer cache.Indexer +} + +// NewGitHubIdentityProviderLister returns a new GitHubIdentityProviderLister. +func NewGitHubIdentityProviderLister(indexer cache.Indexer) GitHubIdentityProviderLister { + return &gitHubIdentityProviderLister{indexer: indexer} +} + +// List lists all GitHubIdentityProviders in the indexer. +func (s *gitHubIdentityProviderLister) List(selector labels.Selector) (ret []*v1alpha1.GitHubIdentityProvider, err error) { + err = cache.ListAll(s.indexer, selector, func(m interface{}) { + ret = append(ret, m.(*v1alpha1.GitHubIdentityProvider)) + }) + return ret, err +} + +// GitHubIdentityProviders returns an object that can list and get GitHubIdentityProviders. +func (s *gitHubIdentityProviderLister) GitHubIdentityProviders(namespace string) GitHubIdentityProviderNamespaceLister { + return gitHubIdentityProviderNamespaceLister{indexer: s.indexer, namespace: namespace} +} + +// GitHubIdentityProviderNamespaceLister helps list and get GitHubIdentityProviders. +// All objects returned here must be treated as read-only. +type GitHubIdentityProviderNamespaceLister interface { + // List lists all GitHubIdentityProviders in the indexer for a given namespace. + // Objects returned here must be treated as read-only. + List(selector labels.Selector) (ret []*v1alpha1.GitHubIdentityProvider, err error) + // Get retrieves the GitHubIdentityProvider from the indexer for a given namespace and name. + // Objects returned here must be treated as read-only. + Get(name string) (*v1alpha1.GitHubIdentityProvider, error) + GitHubIdentityProviderNamespaceListerExpansion +} + +// gitHubIdentityProviderNamespaceLister implements the GitHubIdentityProviderNamespaceLister +// interface. +type gitHubIdentityProviderNamespaceLister struct { + indexer cache.Indexer + namespace string +} + +// List lists all GitHubIdentityProviders in the indexer for a given namespace. +func (s gitHubIdentityProviderNamespaceLister) List(selector labels.Selector) (ret []*v1alpha1.GitHubIdentityProvider, err error) { + err = cache.ListAllByNamespace(s.indexer, s.namespace, selector, func(m interface{}) { + ret = append(ret, m.(*v1alpha1.GitHubIdentityProvider)) + }) + return ret, err +} + +// Get retrieves the GitHubIdentityProvider from the indexer for a given namespace and name. +func (s gitHubIdentityProviderNamespaceLister) Get(name string) (*v1alpha1.GitHubIdentityProvider, error) { + obj, exists, err := s.indexer.GetByKey(s.namespace + "/" + name) + if err != nil { + return nil, err + } + if !exists { + return nil, errors.NewNotFound(v1alpha1.Resource("githubidentityprovider"), name) + } + return obj.(*v1alpha1.GitHubIdentityProvider), nil +} diff --git a/generated/1.30/client/supervisor/openapi/zz_generated.openapi.go b/generated/1.30/client/supervisor/openapi/zz_generated.openapi.go index 231ceea81..62961f47f 100644 --- a/generated/1.30/client/supervisor/openapi/zz_generated.openapi.go +++ b/generated/1.30/client/supervisor/openapi/zz_generated.openapi.go @@ -1591,7 +1591,8 @@ func schema_k8sio_api_core_v1_ConfigMapEnvSource(ref common.ReferenceCallback) c Properties: map[string]spec.Schema{ "name": { SchemaProps: spec.SchemaProps{ - Description: "Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names", + Description: "Name of the referent. This field is effectively required, but due to backwards compatibility is allowed to be empty. Instances of this type with an empty value here are almost certainly wrong. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names", + Default: "", Type: []string{"string"}, Format: "", }, @@ -1618,7 +1619,8 @@ func schema_k8sio_api_core_v1_ConfigMapKeySelector(ref common.ReferenceCallback) Properties: map[string]spec.Schema{ "name": { SchemaProps: spec.SchemaProps{ - Description: "Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names", + Description: "Name of the referent. This field is effectively required, but due to backwards compatibility is allowed to be empty. Instances of this type with an empty value here are almost certainly wrong. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names", + Default: "", Type: []string{"string"}, Format: "", }, @@ -1762,7 +1764,8 @@ func schema_k8sio_api_core_v1_ConfigMapProjection(ref common.ReferenceCallback) Properties: map[string]spec.Schema{ "name": { SchemaProps: spec.SchemaProps{ - Description: "Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names", + Description: "Name of the referent. This field is effectively required, but due to backwards compatibility is allowed to be empty. Instances of this type with an empty value here are almost certainly wrong. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names", + Default: "", Type: []string{"string"}, Format: "", }, @@ -1810,7 +1813,8 @@ func schema_k8sio_api_core_v1_ConfigMapVolumeSource(ref common.ReferenceCallback Properties: map[string]spec.Schema{ "name": { SchemaProps: spec.SchemaProps{ - Description: "Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names", + Description: "Name of the referent. This field is effectively required, but due to backwards compatibility is allowed to be empty. Instances of this type with an empty value here are almost certainly wrong. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names", + Default: "", Type: []string{"string"}, Format: "", }, @@ -4520,6 +4524,7 @@ func schema_k8sio_api_core_v1_HostAlias(ref common.ReferenceCallback) common.Ope "ip": { SchemaProps: spec.SchemaProps{ Description: "IP address of the host file entry.", + Default: "", Type: []string{"string"}, Format: "", }, @@ -4545,6 +4550,7 @@ func schema_k8sio_api_core_v1_HostAlias(ref common.ReferenceCallback) common.Ope }, }, }, + Required: []string{"ip"}, }, }, } @@ -5290,7 +5296,8 @@ func schema_k8sio_api_core_v1_LocalObjectReference(ref common.ReferenceCallback) Properties: map[string]spec.Schema{ "name": { SchemaProps: spec.SchemaProps{ - Description: "Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names", + Description: "Name of the referent. This field is effectively required, but due to backwards compatibility is allowed to be empty. Instances of this type with an empty value here are almost certainly wrong. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names", + Default: "", Type: []string{"string"}, Format: "", }, @@ -11243,7 +11250,8 @@ func schema_k8sio_api_core_v1_SecretEnvSource(ref common.ReferenceCallback) comm Properties: map[string]spec.Schema{ "name": { SchemaProps: spec.SchemaProps{ - Description: "Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names", + Description: "Name of the referent. This field is effectively required, but due to backwards compatibility is allowed to be empty. Instances of this type with an empty value here are almost certainly wrong. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names", + Default: "", Type: []string{"string"}, Format: "", }, @@ -11270,7 +11278,8 @@ func schema_k8sio_api_core_v1_SecretKeySelector(ref common.ReferenceCallback) co Properties: map[string]spec.Schema{ "name": { SchemaProps: spec.SchemaProps{ - Description: "Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names", + Description: "Name of the referent. This field is effectively required, but due to backwards compatibility is allowed to be empty. Instances of this type with an empty value here are almost certainly wrong. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names", + Default: "", Type: []string{"string"}, Format: "", }, @@ -11362,7 +11371,8 @@ func schema_k8sio_api_core_v1_SecretProjection(ref common.ReferenceCallback) com Properties: map[string]spec.Schema{ "name": { SchemaProps: spec.SchemaProps{ - Description: "Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names", + Description: "Name of the referent. This field is effectively required, but due to backwards compatibility is allowed to be empty. Instances of this type with an empty value here are almost certainly wrong. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names", + Default: "", Type: []string{"string"}, Format: "", }, @@ -12218,7 +12228,7 @@ func schema_k8sio_api_core_v1_ServiceSpec(ref common.ReferenceCallback) common.O }, "trafficDistribution": { SchemaProps: spec.SchemaProps{ - Description: "TrafficDistribution offers a way to express preferences for how traffic is distributed to Service endpoints. Implementations can use this field as a hint, but are not required to guarantee strict adherence. If the field is not set, the implementation will apply its default routing strategy. If set to \"PreferClose\", implementations should prioritize endpoints that are topologically close (e.g., same zone).", + Description: "TrafficDistribution offers a way to express preferences for how traffic is distributed to Service endpoints. Implementations can use this field as a hint, but are not required to guarantee strict adherence. If the field is not set, the implementation will apply its default routing strategy. If set to \"PreferClose\", implementations should prioritize endpoints that are topologically close (e.g., same zone). This is an alpha field and requires enabling ServiceTrafficDistribution feature.", Type: []string{"string"}, Format: "", }, diff --git a/generated/1.30/crds/authentication.concierge.pinniped.dev_jwtauthenticators.yaml b/generated/1.30/crds/authentication.concierge.pinniped.dev_jwtauthenticators.yaml index b464cfa2b..d59fcb783 100644 --- a/generated/1.30/crds/authentication.concierge.pinniped.dev_jwtauthenticators.yaml +++ b/generated/1.30/crds/authentication.concierge.pinniped.dev_jwtauthenticators.yaml @@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - controller-gen.kubebuilder.io/version: v0.14.0 + controller-gen.kubebuilder.io/version: v0.15.0 name: jwtauthenticators.authentication.concierge.pinniped.dev spec: group: authentication.concierge.pinniped.dev diff --git a/generated/1.30/crds/authentication.concierge.pinniped.dev_webhookauthenticators.yaml b/generated/1.30/crds/authentication.concierge.pinniped.dev_webhookauthenticators.yaml index e8cbc6790..4ccd53770 100644 --- a/generated/1.30/crds/authentication.concierge.pinniped.dev_webhookauthenticators.yaml +++ b/generated/1.30/crds/authentication.concierge.pinniped.dev_webhookauthenticators.yaml @@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - controller-gen.kubebuilder.io/version: v0.14.0 + controller-gen.kubebuilder.io/version: v0.15.0 name: webhookauthenticators.authentication.concierge.pinniped.dev spec: group: authentication.concierge.pinniped.dev diff --git a/generated/1.30/crds/config.concierge.pinniped.dev_credentialissuers.yaml b/generated/1.30/crds/config.concierge.pinniped.dev_credentialissuers.yaml index 8d77a6665..26fed7376 100644 --- a/generated/1.30/crds/config.concierge.pinniped.dev_credentialissuers.yaml +++ b/generated/1.30/crds/config.concierge.pinniped.dev_credentialissuers.yaml @@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - controller-gen.kubebuilder.io/version: v0.14.0 + controller-gen.kubebuilder.io/version: v0.15.0 name: credentialissuers.config.concierge.pinniped.dev spec: group: config.concierge.pinniped.dev diff --git a/generated/1.30/crds/config.supervisor.pinniped.dev_federationdomains.yaml b/generated/1.30/crds/config.supervisor.pinniped.dev_federationdomains.yaml index 7bf231443..033513431 100644 --- a/generated/1.30/crds/config.supervisor.pinniped.dev_federationdomains.yaml +++ b/generated/1.30/crds/config.supervisor.pinniped.dev_federationdomains.yaml @@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - controller-gen.kubebuilder.io/version: v0.14.0 + controller-gen.kubebuilder.io/version: v0.15.0 name: federationdomains.config.supervisor.pinniped.dev spec: group: config.supervisor.pinniped.dev @@ -421,10 +421,15 @@ spec: exist. properties: name: + default: "" description: |- Name of the referent. - More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. TODO: Add other useful fields. apiVersion, kind, uid? + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Drop `kubebuilder:default` when controller-gen doesn't need it https://github.com/kubernetes-sigs/kubebuilder/issues/3896. type: string type: object x-kubernetes-map-type: atomic @@ -434,10 +439,15 @@ spec: encrypting state parameters is stored. properties: name: + default: "" description: |- Name of the referent. - More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. TODO: Add other useful fields. apiVersion, kind, uid? + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Drop `kubebuilder:default` when controller-gen doesn't need it https://github.com/kubernetes-sigs/kubebuilder/issues/3896. type: string type: object x-kubernetes-map-type: atomic @@ -447,10 +457,15 @@ spec: signing state parameters is stored. properties: name: + default: "" description: |- Name of the referent. - More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. TODO: Add other useful fields. apiVersion, kind, uid? + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Drop `kubebuilder:default` when controller-gen doesn't need it https://github.com/kubernetes-sigs/kubebuilder/issues/3896. type: string type: object x-kubernetes-map-type: atomic @@ -460,10 +475,15 @@ spec: signing tokens is stored. properties: name: + default: "" description: |- Name of the referent. - More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. TODO: Add other useful fields. apiVersion, kind, uid? + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Drop `kubebuilder:default` when controller-gen doesn't need it https://github.com/kubernetes-sigs/kubebuilder/issues/3896. type: string type: object x-kubernetes-map-type: atomic diff --git a/generated/1.30/crds/config.supervisor.pinniped.dev_oidcclients.yaml b/generated/1.30/crds/config.supervisor.pinniped.dev_oidcclients.yaml index d33b31bf1..c44ecbf1e 100644 --- a/generated/1.30/crds/config.supervisor.pinniped.dev_oidcclients.yaml +++ b/generated/1.30/crds/config.supervisor.pinniped.dev_oidcclients.yaml @@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - controller-gen.kubebuilder.io/version: v0.14.0 + controller-gen.kubebuilder.io/version: v0.15.0 name: oidcclients.config.supervisor.pinniped.dev spec: group: config.supervisor.pinniped.dev diff --git a/generated/1.30/crds/idp.supervisor.pinniped.dev_activedirectoryidentityproviders.yaml b/generated/1.30/crds/idp.supervisor.pinniped.dev_activedirectoryidentityproviders.yaml index 414ac4f51..062251102 100644 --- a/generated/1.30/crds/idp.supervisor.pinniped.dev_activedirectoryidentityproviders.yaml +++ b/generated/1.30/crds/idp.supervisor.pinniped.dev_activedirectoryidentityproviders.yaml @@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - controller-gen.kubebuilder.io/version: v0.14.0 + controller-gen.kubebuilder.io/version: v0.15.0 name: activedirectoryidentityproviders.idp.supervisor.pinniped.dev spec: group: idp.supervisor.pinniped.dev diff --git a/generated/1.30/crds/idp.supervisor.pinniped.dev_githubidentityproviders.yaml b/generated/1.30/crds/idp.supervisor.pinniped.dev_githubidentityproviders.yaml new file mode 100644 index 000000000..f93108700 --- /dev/null +++ b/generated/1.30/crds/idp.supervisor.pinniped.dev_githubidentityproviders.yaml @@ -0,0 +1,326 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.15.0 + name: githubidentityproviders.idp.supervisor.pinniped.dev +spec: + group: idp.supervisor.pinniped.dev + names: + categories: + - pinniped + - pinniped-idp + - pinniped-idps + kind: GitHubIdentityProvider + listKind: GitHubIdentityProviderList + plural: githubidentityproviders + singular: githubidentityprovider + scope: Namespaced + versions: + - additionalPrinterColumns: + - jsonPath: .spec.githubAPI.host + name: Host + type: string + - jsonPath: .status.phase + name: Status + type: string + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1alpha1 + schema: + openAPIV3Schema: + description: |- + GitHubIdentityProvider describes the configuration of an upstream GitHub identity provider. + This upstream provider can be configured with either a GitHub App or a GitHub OAuth2 App. + + + Right now, only web-based logins are supported, for both the pinniped-cli client and clients configured + as OIDCClients. + 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: + description: Spec for configuring the identity provider. + properties: + allowAuthentication: + description: AllowAuthentication allows customization of who can authenticate + using this IDP and how. + properties: + organizations: + description: Organizations allows customization of which organizations + can authenticate using this IDP. + properties: + allowed: + description: |- + Allowed, when specified, indicates that only users with membership in at least one of the listed + GitHub organizations may log in. In addition, the group membership presented to Kubernetes will only include + teams within the listed GitHub organizations. Additional login rules or group filtering can optionally be + provided as policy expression on any Pinniped Supervisor FederationDomain that includes this IDP. + + + The configured GitHub App or GitHub OAuth App must be allowed to see membership in the listed organizations, + otherwise Pinniped will not be aware that the user belongs to the listed organization or any teams + within that organization. + + + If no organizations are listed, you must set organizations: AllGitHubUsers. + items: + type: string + maxItems: 64 + type: array + x-kubernetes-list-type: set + policy: + default: OnlyUsersFromAllowedOrganizations + description: |- + Policy must be set to "AllGitHubUsers" if allowed is empty. + + + This field only exists to ensure that Pinniped administrators are aware that an empty list of + allowedOrganizations means all GitHub users are allowed to log in. + enum: + - OnlyUsersFromAllowedOrganizations + - AllGitHubUsers + type: string + type: object + x-kubernetes-validations: + - message: spec.allowAuthentication.organizations.policy must + be 'OnlyUsersFromAllowedOrganizations' when spec.allowAuthentication.organizations.allowed + has organizations listed + rule: '!(has(self.allowed) && size(self.allowed) > 0 && self.policy + == ''AllGitHubUsers'')' + - message: spec.allowAuthentication.organizations.policy must + be 'AllGitHubUsers' when spec.allowAuthentication.organizations.allowed + is empty + rule: '!((!has(self.allowed) || size(self.allowed) == 0) && + self.policy == ''OnlyUsersFromAllowedOrganizations'')' + required: + - organizations + type: object + claims: + default: {} + description: Claims allows customization of the username and groups + claims. + properties: + groups: + default: slug + description: |- + Groups configures which property of the GitHub team record shall determine the group names in Kubernetes. + + + Can be either "name" or "slug". Defaults to "slug". + + + GitHub team names can contain upper and lower case characters, whitespace, and punctuation (e.g. "Kube admins!"). + + + GitHub team slugs are lower case alphanumeric characters and may contain dashes and underscores (e.g. "kube-admins"). + + + Group names as presented to Kubernetes will always be prefixed by the GitHub organization name followed by a + forward slash (e.g. "my-org/my-team"). GitHub organization login names can only contain alphanumeric characters + or single hyphens, so the first forward slash `/` will be the separator between the organization login name and + the team name or slug. + + + If desired, an admin could configure identity transformation expressions on the Pinniped Supervisor's + FederationDomain to further customize how these group names are presented to Kubernetes. + + + See the response schema for + [List teams for the authenticated user](https://docs.github.com/en/rest/teams/teams?apiVersion=2022-11-28#list-teams-for-the-authenticated-user). + enum: + - name + - slug + type: string + username: + default: login:id + description: |- + Username configures which property of the GitHub user record shall determine the username in Kubernetes. + + + Can be either "id", "login", or "login:id". Defaults to "login:id". + + + GitHub's user login attributes can only contain alphanumeric characters and non-repeating hyphens, + and may not start or end with hyphens. GitHub users are allowed to change their login name, + although it is inconvenient. If a GitHub user changed their login name from "foo" to "bar", + then a second user might change their name from "baz" to "foo" in order to take the old + username of the first user. For this reason, it is not as safe to make authorization decisions + based only on the user's login attribute. + + + If desired, an admin could configure identity transformation expressions on the Pinniped Supervisor's + FederationDomain to further customize how these usernames are presented to Kubernetes. + + + Defaults to "login:id", which is the user login attribute, followed by a colon, followed by the unique and + unchanging integer ID number attribute. This blends human-readable login names with the unchanging ID value + from GitHub. Colons are not allowed in GitHub login attributes or ID numbers, so this is a reasonable + choice to concatenate the two values. + + + See the response schema for + [Get the authenticated user](https://docs.github.com/en/rest/users/users?apiVersion=2022-11-28#get-the-authenticated-user). + enum: + - id + - login + - login:id + type: string + type: object + client: + description: Client identifies the secret with credentials for a GitHub + App or GitHub OAuth2 App (a GitHub client). + properties: + secretName: + description: |- + SecretName contains the name of a namespace-local Secret object that provides the clientID and + clientSecret for an GitHub App or GitHub OAuth2 client. + + + This secret must be of type "secrets.pinniped.dev/github-client" with keys "clientID" and "clientSecret". + minLength: 1 + type: string + required: + - secretName + type: object + githubAPI: + default: {} + description: GitHubAPI allows configuration for GitHub Enterprise + Server + properties: + host: + default: github.com + description: |- + Host is required only for GitHub Enterprise Server. + Defaults to using GitHub's public API ("github.com"). + Do not specify a protocol or scheme since "https://" will always be used. + Port is optional. Do not specify a path, query, fragment, or userinfo. + Only domain name or IP address, subdomains (optional), and port (optional). + IPv4 and IPv6 are supported. If using an IPv6 address with a port, you must enclose the IPv6 address + in square brackets. Example: "[::1]:443". + minLength: 1 + type: string + tls: + description: TLS configuration for GitHub Enterprise Server. + properties: + certificateAuthorityData: + description: X.509 Certificate Authority (base64-encoded PEM + bundle). If omitted, a default set of system roots will + be trusted. + type: string + type: object + type: object + required: + - allowAuthentication + - client + type: object + status: + description: Status of the identity provider. + properties: + conditions: + description: Conditions represents the observations of an identity + provider's current state. + items: + description: "Condition contains details for one aspect of the current + state of this API Resource.\n---\nThis struct is intended for + direct use as an array at the field path .status.conditions. For + example,\n\n\n\ttype FooStatus struct{\n\t // Represents the + observations of a foo's current state.\n\t // Known .status.conditions.type + are: \"Available\", \"Progressing\", and \"Degraded\"\n\t // + +patchMergeKey=type\n\t // +patchStrategy=merge\n\t // +listType=map\n\t + \ // +listMapKey=type\n\t Conditions []metav1.Condition `json:\"conditions,omitempty\" + patchStrategy:\"merge\" patchMergeKey:\"type\" protobuf:\"bytes,1,rep,name=conditions\"`\n\n\n\t + \ // other fields\n\t}" + properties: + lastTransitionTime: + description: |- + lastTransitionTime is the last time the condition transitioned from one status to another. + This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: |- + message is a human readable message indicating details about the transition. + This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: |- + observedGeneration represents the .metadata.generation that the condition was set based upon. + For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date + with respect to the current state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: |- + reason contains a programmatic identifier indicating the reason for the condition's last transition. + Producers of specific condition types may define expected values and meanings for this field, + and whether the values are considered a guaranteed API. + The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: |- + type of condition in CamelCase or in foo.example.com/CamelCase. + --- + Many .condition.type values are consistent across resources like Available, but because arbitrary conditions can be + useful (see .node.status.conditions), the ability to deconflict is important. + The regex it matches is (dns1123SubdomainFmt/)?(qualifiedNameFmt) + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + x-kubernetes-list-map-keys: + - type + x-kubernetes-list-type: map + phase: + default: Pending + description: Phase summarizes the overall status of the GitHubIdentityProvider. + enum: + - Pending + - Ready + - Error + type: string + type: object + required: + - spec + type: object + served: true + storage: true + subresources: + status: {} diff --git a/generated/1.30/crds/idp.supervisor.pinniped.dev_ldapidentityproviders.yaml b/generated/1.30/crds/idp.supervisor.pinniped.dev_ldapidentityproviders.yaml index f718e93aa..711e9a754 100644 --- a/generated/1.30/crds/idp.supervisor.pinniped.dev_ldapidentityproviders.yaml +++ b/generated/1.30/crds/idp.supervisor.pinniped.dev_ldapidentityproviders.yaml @@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - controller-gen.kubebuilder.io/version: v0.14.0 + controller-gen.kubebuilder.io/version: v0.15.0 name: ldapidentityproviders.idp.supervisor.pinniped.dev spec: group: idp.supervisor.pinniped.dev diff --git a/generated/1.30/crds/idp.supervisor.pinniped.dev_oidcidentityproviders.yaml b/generated/1.30/crds/idp.supervisor.pinniped.dev_oidcidentityproviders.yaml index 486665605..acfca1573 100644 --- a/generated/1.30/crds/idp.supervisor.pinniped.dev_oidcidentityproviders.yaml +++ b/generated/1.30/crds/idp.supervisor.pinniped.dev_oidcidentityproviders.yaml @@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - controller-gen.kubebuilder.io/version: v0.14.0 + controller-gen.kubebuilder.io/version: v0.15.0 name: oidcidentityproviders.idp.supervisor.pinniped.dev spec: group: idp.supervisor.pinniped.dev diff --git a/generated/latest/README.adoc b/generated/latest/README.adoc index 3b9dd2991..337aacd2a 100644 --- a/generated/latest/README.adoc +++ b/generated/latest/README.adoc @@ -1645,6 +1645,285 @@ Optional, when empty this defaults to "objectGUID". + |=== +[id="{anchor_prefix}-go-pinniped-dev-generated-1-30-apis-supervisor-idp-v1alpha1-githubapiconfig"] +==== GitHubAPIConfig + +GitHubAPIConfig allows configuration for GitHub Enterprise Server + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-30-apis-supervisor-idp-v1alpha1-githubidentityproviderspec[$$GitHubIdentityProviderSpec$$] +**** + +[cols="25a,75a", options="header"] +|=== +| Field | Description +| *`host`* __string__ | Host is required only for GitHub Enterprise Server. + +Defaults to using GitHub's public API ("github.com"). + +Do not specify a protocol or scheme since "https://" will always be used. + +Port is optional. Do not specify a path, query, fragment, or userinfo. + +Only domain name or IP address, subdomains (optional), and port (optional). + +IPv4 and IPv6 are supported. If using an IPv6 address with a port, you must enclose the IPv6 address + +in square brackets. Example: "[::1]:443". + +| *`tls`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-30-apis-supervisor-idp-v1alpha1-tlsspec[$$TLSSpec$$]__ | TLS configuration for GitHub Enterprise Server. + +|=== + + +[id="{anchor_prefix}-go-pinniped-dev-generated-1-30-apis-supervisor-idp-v1alpha1-githuballowauthenticationspec"] +==== GitHubAllowAuthenticationSpec + +GitHubAllowAuthenticationSpec allows customization of who can authenticate using this IDP and how. + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-30-apis-supervisor-idp-v1alpha1-githubidentityproviderspec[$$GitHubIdentityProviderSpec$$] +**** + +[cols="25a,75a", options="header"] +|=== +| Field | Description +| *`organizations`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-30-apis-supervisor-idp-v1alpha1-githuborganizationsspec[$$GitHubOrganizationsSpec$$]__ | Organizations allows customization of which organizations can authenticate using this IDP. + +|=== + + +[id="{anchor_prefix}-go-pinniped-dev-generated-1-30-apis-supervisor-idp-v1alpha1-githuballowedauthorganizationspolicy"] +==== GitHubAllowedAuthOrganizationsPolicy (string) + + + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-30-apis-supervisor-idp-v1alpha1-githuborganizationsspec[$$GitHubOrganizationsSpec$$] +**** + + + +[id="{anchor_prefix}-go-pinniped-dev-generated-1-30-apis-supervisor-idp-v1alpha1-githubclaims"] +==== GitHubClaims + +GitHubClaims allows customization of the username and groups claims. + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-30-apis-supervisor-idp-v1alpha1-githubidentityproviderspec[$$GitHubIdentityProviderSpec$$] +**** + +[cols="25a,75a", options="header"] +|=== +| Field | Description +| *`username`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-30-apis-supervisor-idp-v1alpha1-githubusernameattribute[$$GitHubUsernameAttribute$$]__ | Username configures which property of the GitHub user record shall determine the username in Kubernetes. + + + +Can be either "id", "login", or "login:id". Defaults to "login:id". + + + +GitHub's user login attributes can only contain alphanumeric characters and non-repeating hyphens, + +and may not start or end with hyphens. GitHub users are allowed to change their login name, + +although it is inconvenient. If a GitHub user changed their login name from "foo" to "bar", + +then a second user might change their name from "baz" to "foo" in order to take the old + +username of the first user. For this reason, it is not as safe to make authorization decisions + +based only on the user's login attribute. + + + +If desired, an admin could configure identity transformation expressions on the Pinniped Supervisor's + +FederationDomain to further customize how these usernames are presented to Kubernetes. + + + +Defaults to "login:id", which is the user login attribute, followed by a colon, followed by the unique and + +unchanging integer ID number attribute. This blends human-readable login names with the unchanging ID value + +from GitHub. Colons are not allowed in GitHub login attributes or ID numbers, so this is a reasonable + +choice to concatenate the two values. + + + +See the response schema for + +[Get the authenticated user](https://docs.github.com/en/rest/users/users?apiVersion=2022-11-28#get-the-authenticated-user). + +| *`groups`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-30-apis-supervisor-idp-v1alpha1-githubgroupnameattribute[$$GitHubGroupNameAttribute$$]__ | Groups configures which property of the GitHub team record shall determine the group names in Kubernetes. + + + +Can be either "name" or "slug". Defaults to "slug". + + + +GitHub team names can contain upper and lower case characters, whitespace, and punctuation (e.g. "Kube admins!"). + + + +GitHub team slugs are lower case alphanumeric characters and may contain dashes and underscores (e.g. "kube-admins"). + + + +Group names as presented to Kubernetes will always be prefixed by the GitHub organization name followed by a + +forward slash (e.g. "my-org/my-team"). GitHub organization login names can only contain alphanumeric characters + +or single hyphens, so the first forward slash `/` will be the separator between the organization login name and + +the team name or slug. + + + +If desired, an admin could configure identity transformation expressions on the Pinniped Supervisor's + +FederationDomain to further customize how these group names are presented to Kubernetes. + + + +See the response schema for + +[List teams for the authenticated user](https://docs.github.com/en/rest/teams/teams?apiVersion=2022-11-28#list-teams-for-the-authenticated-user). + +|=== + + +[id="{anchor_prefix}-go-pinniped-dev-generated-1-30-apis-supervisor-idp-v1alpha1-githubclientspec"] +==== GitHubClientSpec + +GitHubClientSpec contains information about the GitHub client that this identity provider will use +for web-based login flows. + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-30-apis-supervisor-idp-v1alpha1-githubidentityproviderspec[$$GitHubIdentityProviderSpec$$] +**** + +[cols="25a,75a", options="header"] +|=== +| Field | Description +| *`secretName`* __string__ | SecretName contains the name of a namespace-local Secret object that provides the clientID and + +clientSecret for an GitHub App or GitHub OAuth2 client. + + + +This secret must be of type "secrets.pinniped.dev/github-client" with keys "clientID" and "clientSecret". + +|=== + + +[id="{anchor_prefix}-go-pinniped-dev-generated-1-30-apis-supervisor-idp-v1alpha1-githubgroupnameattribute"] +==== GitHubGroupNameAttribute (string) + +GitHubGroupNameAttribute allows the user to specify which attribute from GitHub to use for the group +names to present to Kubernetes. See the response schema for +[List teams for the authenticated user](https://docs.github.com/en/rest/teams/teams?apiVersion=2022-11-28#list-teams-for-the-authenticated-user). + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-30-apis-supervisor-idp-v1alpha1-githubclaims[$$GitHubClaims$$] +**** + + + +[id="{anchor_prefix}-go-pinniped-dev-generated-1-30-apis-supervisor-idp-v1alpha1-githubidentityprovider"] +==== GitHubIdentityProvider + +GitHubIdentityProvider describes the configuration of an upstream GitHub identity provider. +This upstream provider can be configured with either a GitHub App or a GitHub OAuth2 App. + + +Right now, only web-based logins are supported, for both the pinniped-cli client and clients configured +as OIDCClients. + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-30-apis-supervisor-idp-v1alpha1-githubidentityproviderlist[$$GitHubIdentityProviderList$$] +**** + +[cols="25a,75a", options="header"] +|=== +| Field | Description +| *`metadata`* __link:https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.3/#objectmeta-v1-meta[$$ObjectMeta$$]__ | Refer to Kubernetes API documentation for fields of `metadata`. + +| *`spec`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-30-apis-supervisor-idp-v1alpha1-githubidentityproviderspec[$$GitHubIdentityProviderSpec$$]__ | Spec for configuring the identity provider. + +| *`status`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-30-apis-supervisor-idp-v1alpha1-githubidentityproviderstatus[$$GitHubIdentityProviderStatus$$]__ | Status of the identity provider. + +|=== + + + + +[id="{anchor_prefix}-go-pinniped-dev-generated-1-30-apis-supervisor-idp-v1alpha1-githubidentityproviderphase"] +==== GitHubIdentityProviderPhase (string) + + + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-30-apis-supervisor-idp-v1alpha1-githubidentityproviderstatus[$$GitHubIdentityProviderStatus$$] +**** + + + +[id="{anchor_prefix}-go-pinniped-dev-generated-1-30-apis-supervisor-idp-v1alpha1-githubidentityproviderspec"] +==== GitHubIdentityProviderSpec + +GitHubIdentityProviderSpec is the spec for configuring an GitHub identity provider. + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-30-apis-supervisor-idp-v1alpha1-githubidentityprovider[$$GitHubIdentityProvider$$] +**** + +[cols="25a,75a", options="header"] +|=== +| Field | Description +| *`githubAPI`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-30-apis-supervisor-idp-v1alpha1-githubapiconfig[$$GitHubAPIConfig$$]__ | GitHubAPI allows configuration for GitHub Enterprise Server + +| *`claims`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-30-apis-supervisor-idp-v1alpha1-githubclaims[$$GitHubClaims$$]__ | Claims allows customization of the username and groups claims. + +| *`allowAuthentication`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-30-apis-supervisor-idp-v1alpha1-githuballowauthenticationspec[$$GitHubAllowAuthenticationSpec$$]__ | AllowAuthentication allows customization of who can authenticate using this IDP and how. + +| *`client`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-30-apis-supervisor-idp-v1alpha1-githubclientspec[$$GitHubClientSpec$$]__ | Client identifies the secret with credentials for a GitHub App or GitHub OAuth2 App (a GitHub client). + +|=== + + +[id="{anchor_prefix}-go-pinniped-dev-generated-1-30-apis-supervisor-idp-v1alpha1-githubidentityproviderstatus"] +==== GitHubIdentityProviderStatus + +GitHubIdentityProviderStatus is the status of an GitHub identity provider. + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-30-apis-supervisor-idp-v1alpha1-githubidentityprovider[$$GitHubIdentityProvider$$] +**** + +[cols="25a,75a", options="header"] +|=== +| Field | Description +| *`phase`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-30-apis-supervisor-idp-v1alpha1-githubidentityproviderphase[$$GitHubIdentityProviderPhase$$]__ | Phase summarizes the overall status of the GitHubIdentityProvider. + +| *`conditions`* __link:https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.3/#condition-v1-meta[$$Condition$$] array__ | Conditions represents the observations of an identity provider's current state. + +|=== + + +[id="{anchor_prefix}-go-pinniped-dev-generated-1-30-apis-supervisor-idp-v1alpha1-githuborganizationsspec"] +==== GitHubOrganizationsSpec + + + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-30-apis-supervisor-idp-v1alpha1-githuballowauthenticationspec[$$GitHubAllowAuthenticationSpec$$] +**** + +[cols="25a,75a", options="header"] +|=== +| Field | Description +| *`policy`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-30-apis-supervisor-idp-v1alpha1-githuballowedauthorganizationspolicy[$$GitHubAllowedAuthOrganizationsPolicy$$]__ | Policy must be set to "AllGitHubUsers" if allowed is empty. + + + +This field only exists to ensure that Pinniped administrators are aware that an empty list of + +allowedOrganizations means all GitHub users are allowed to log in. + +| *`allowed`* __string array__ | Allowed, when specified, indicates that only users with membership in at least one of the listed + +GitHub organizations may log in. In addition, the group membership presented to Kubernetes will only include + +teams within the listed GitHub organizations. Additional login rules or group filtering can optionally be + +provided as policy expression on any Pinniped Supervisor FederationDomain that includes this IDP. + + + +The configured GitHub App or GitHub OAuth App must be allowed to see membership in the listed organizations, + +otherwise Pinniped will not be aware that the user belongs to the listed organization or any teams + +within that organization. + + + +If no organizations are listed, you must set organizations: AllGitHubUsers. + +|=== + + +[id="{anchor_prefix}-go-pinniped-dev-generated-1-30-apis-supervisor-idp-v1alpha1-githubusernameattribute"] +==== GitHubUsernameAttribute (string) + +GitHubUsernameAttribute allows the user to specify which attribute(s) from GitHub to use for the username to present +to Kubernetes. See the response schema for +[Get the authenticated user](https://docs.github.com/en/rest/users/users?apiVersion=2022-11-28#get-the-authenticated-user). + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-30-apis-supervisor-idp-v1alpha1-githubclaims[$$GitHubClaims$$] +**** + + + [id="{anchor_prefix}-go-pinniped-dev-generated-1-30-apis-supervisor-idp-v1alpha1-ldapidentityprovider"] ==== LDAPIdentityProvider @@ -2108,11 +2387,12 @@ Parameter is a key/value pair which represents a parameter in an HTTP request. [id="{anchor_prefix}-go-pinniped-dev-generated-1-30-apis-supervisor-idp-v1alpha1-tlsspec"] ==== TLSSpec -Configuration for TLS parameters related to identity provider integration. +TLSSpec provides TLS configuration for identity provider integration. .Appears In: **** - xref:{anchor_prefix}-go-pinniped-dev-generated-1-30-apis-supervisor-idp-v1alpha1-activedirectoryidentityproviderspec[$$ActiveDirectoryIdentityProviderSpec$$] +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-30-apis-supervisor-idp-v1alpha1-githubapiconfig[$$GitHubAPIConfig$$] - xref:{anchor_prefix}-go-pinniped-dev-generated-1-30-apis-supervisor-idp-v1alpha1-ldapidentityproviderspec[$$LDAPIdentityProviderSpec$$] - xref:{anchor_prefix}-go-pinniped-dev-generated-1-30-apis-supervisor-idp-v1alpha1-oidcidentityproviderspec[$$OIDCIdentityProviderSpec$$] **** diff --git a/generated/latest/apis/supervisor/idp/v1alpha1/register.go b/generated/latest/apis/supervisor/idp/v1alpha1/register.go index 8829a8638..705be8076 100644 --- a/generated/latest/apis/supervisor/idp/v1alpha1/register.go +++ b/generated/latest/apis/supervisor/idp/v1alpha1/register.go @@ -1,4 +1,4 @@ -// Copyright 2020-2022 the Pinniped contributors. All Rights Reserved. +// Copyright 2020-2024 the Pinniped contributors. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 package v1alpha1 @@ -36,6 +36,8 @@ func addKnownTypes(scheme *runtime.Scheme) error { &LDAPIdentityProviderList{}, &ActiveDirectoryIdentityProvider{}, &ActiveDirectoryIdentityProviderList{}, + &GitHubIdentityProvider{}, + &GitHubIdentityProviderList{}, ) metav1.AddToGroupVersion(scheme, SchemeGroupVersion) return nil diff --git a/generated/latest/apis/supervisor/idp/v1alpha1/types_githubidentityprovider.go b/generated/latest/apis/supervisor/idp/v1alpha1/types_githubidentityprovider.go new file mode 100644 index 000000000..c84f46dbd --- /dev/null +++ b/generated/latest/apis/supervisor/idp/v1alpha1/types_githubidentityprovider.go @@ -0,0 +1,256 @@ +// Copyright 2024 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package v1alpha1 + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +type GitHubIdentityProviderPhase string + +const ( + // GitHubPhasePending is the default phase for newly-created GitHubIdentityProvider resources. + GitHubPhasePending GitHubIdentityProviderPhase = "Pending" + + // GitHubPhaseReady is the phase for an GitHubIdentityProvider resource in a healthy state. + GitHubPhaseReady GitHubIdentityProviderPhase = "Ready" + + // GitHubPhaseError is the phase for an GitHubIdentityProvider in an unhealthy state. + GitHubPhaseError GitHubIdentityProviderPhase = "Error" +) + +type GitHubAllowedAuthOrganizationsPolicy string + +const ( + // GitHubAllowedAuthOrganizationsPolicyAllGitHubUsers means any GitHub user is allowed to log in using this identity + // provider, regardless of their organization membership or lack thereof. + GitHubAllowedAuthOrganizationsPolicyAllGitHubUsers GitHubAllowedAuthOrganizationsPolicy = "AllGitHubUsers" + + // GitHubAllowedAuthOrganizationsPolicyOnlyUsersFromAllowedOrganizations means only those users with membership in + // the listed GitHub organizations are allowed to log in. + GitHubAllowedAuthOrganizationsPolicyOnlyUsersFromAllowedOrganizations GitHubAllowedAuthOrganizationsPolicy = "OnlyUsersFromAllowedOrganizations" +) + +// GitHubIdentityProviderStatus is the status of an GitHub identity provider. +type GitHubIdentityProviderStatus struct { + // Phase summarizes the overall status of the GitHubIdentityProvider. + // + // +kubebuilder:default=Pending + // +kubebuilder:validation:Enum=Pending;Ready;Error + Phase GitHubIdentityProviderPhase `json:"phase,omitempty"` + + // Conditions represents the observations of an identity provider's current state. + // + // +patchMergeKey=type + // +patchStrategy=merge + // +listType=map + // +listMapKey=type + Conditions []metav1.Condition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type"` +} + +// GitHubAPIConfig allows configuration for GitHub Enterprise Server +type GitHubAPIConfig struct { + // Host is required only for GitHub Enterprise Server. + // Defaults to using GitHub's public API ("github.com"). + // Do not specify a protocol or scheme since "https://" will always be used. + // Port is optional. Do not specify a path, query, fragment, or userinfo. + // Only domain name or IP address, subdomains (optional), and port (optional). + // IPv4 and IPv6 are supported. If using an IPv6 address with a port, you must enclose the IPv6 address + // in square brackets. Example: "[::1]:443". + // + // +kubebuilder:default="github.com" + // +kubebuilder:validation:MinLength=1 + // +optional + Host *string `json:"host"` + + // TLS configuration for GitHub Enterprise Server. + // + // +optional + TLS *TLSSpec `json:"tls,omitempty"` +} + +// GitHubUsernameAttribute allows the user to specify which attribute(s) from GitHub to use for the username to present +// to Kubernetes. See the response schema for +// [Get the authenticated user](https://docs.github.com/en/rest/users/users?apiVersion=2022-11-28#get-the-authenticated-user). +type GitHubUsernameAttribute string + +const ( + // GitHubUsernameID specifies using the `id` attribute from the GitHub user for the username to present to Kubernetes. + GitHubUsernameID GitHubUsernameAttribute = "id" + + // GitHubUsernameLogin specifies using the `login` attribute from the GitHub user as the username to present to Kubernetes. + GitHubUsernameLogin GitHubUsernameAttribute = "login" + + // GitHubUsernameLoginAndID specifies combining the `login` and `id` attributes from the GitHub user as the + // username to present to Kubernetes, separated by a colon. Example: "my-login:1234" + GitHubUsernameLoginAndID GitHubUsernameAttribute = "login:id" +) + +// GitHubGroupNameAttribute allows the user to specify which attribute from GitHub to use for the group +// names to present to Kubernetes. See the response schema for +// [List teams for the authenticated user](https://docs.github.com/en/rest/teams/teams?apiVersion=2022-11-28#list-teams-for-the-authenticated-user). +type GitHubGroupNameAttribute string + +const ( + // GitHubUseTeamNameForGroupName specifies using the GitHub team's `name` attribute as the group name to present to Kubernetes. + GitHubUseTeamNameForGroupName GitHubGroupNameAttribute = "name" + + // GitHubUseTeamSlugForGroupName specifies using the GitHub team's `slug` attribute as the group name to present to Kubernetes. + GitHubUseTeamSlugForGroupName GitHubGroupNameAttribute = "slug" +) + +// GitHubClaims allows customization of the username and groups claims. +type GitHubClaims struct { + // Username configures which property of the GitHub user record shall determine the username in Kubernetes. + // + // Can be either "id", "login", or "login:id". Defaults to "login:id". + // + // GitHub's user login attributes can only contain alphanumeric characters and non-repeating hyphens, + // and may not start or end with hyphens. GitHub users are allowed to change their login name, + // although it is inconvenient. If a GitHub user changed their login name from "foo" to "bar", + // then a second user might change their name from "baz" to "foo" in order to take the old + // username of the first user. For this reason, it is not as safe to make authorization decisions + // based only on the user's login attribute. + // + // If desired, an admin could configure identity transformation expressions on the Pinniped Supervisor's + // FederationDomain to further customize how these usernames are presented to Kubernetes. + // + // Defaults to "login:id", which is the user login attribute, followed by a colon, followed by the unique and + // unchanging integer ID number attribute. This blends human-readable login names with the unchanging ID value + // from GitHub. Colons are not allowed in GitHub login attributes or ID numbers, so this is a reasonable + // choice to concatenate the two values. + // + // See the response schema for + // [Get the authenticated user](https://docs.github.com/en/rest/users/users?apiVersion=2022-11-28#get-the-authenticated-user). + // + // +kubebuilder:default="login:id" + // +kubebuilder:validation:Enum={"id","login","login:id"} + // +optional + Username *GitHubUsernameAttribute `json:"username"` + + // Groups configures which property of the GitHub team record shall determine the group names in Kubernetes. + // + // Can be either "name" or "slug". Defaults to "slug". + // + // GitHub team names can contain upper and lower case characters, whitespace, and punctuation (e.g. "Kube admins!"). + // + // GitHub team slugs are lower case alphanumeric characters and may contain dashes and underscores (e.g. "kube-admins"). + // + // Group names as presented to Kubernetes will always be prefixed by the GitHub organization name followed by a + // forward slash (e.g. "my-org/my-team"). GitHub organization login names can only contain alphanumeric characters + // or single hyphens, so the first forward slash `/` will be the separator between the organization login name and + // the team name or slug. + // + // If desired, an admin could configure identity transformation expressions on the Pinniped Supervisor's + // FederationDomain to further customize how these group names are presented to Kubernetes. + // + // See the response schema for + // [List teams for the authenticated user](https://docs.github.com/en/rest/teams/teams?apiVersion=2022-11-28#list-teams-for-the-authenticated-user). + // + // +kubebuilder:default=slug + // +kubebuilder:validation:Enum=name;slug + // +optional + Groups *GitHubGroupNameAttribute `json:"groups"` +} + +// GitHubClientSpec contains information about the GitHub client that this identity provider will use +// for web-based login flows. +type GitHubClientSpec struct { + // SecretName contains the name of a namespace-local Secret object that provides the clientID and + // clientSecret for an GitHub App or GitHub OAuth2 client. + // + // This secret must be of type "secrets.pinniped.dev/github-client" with keys "clientID" and "clientSecret". + // + // +kubebuilder:validation:MinLength=1 + SecretName string `json:"secretName"` +} + +type GitHubOrganizationsSpec struct { + // Policy must be set to "AllGitHubUsers" if allowed is empty. + // + // This field only exists to ensure that Pinniped administrators are aware that an empty list of + // allowedOrganizations means all GitHub users are allowed to log in. + // + // +kubebuilder:default=OnlyUsersFromAllowedOrganizations + // +kubebuilder:validation:Enum=OnlyUsersFromAllowedOrganizations;AllGitHubUsers + // +optional + Policy *GitHubAllowedAuthOrganizationsPolicy `json:"policy"` + + // Allowed, when specified, indicates that only users with membership in at least one of the listed + // GitHub organizations may log in. In addition, the group membership presented to Kubernetes will only include + // teams within the listed GitHub organizations. Additional login rules or group filtering can optionally be + // provided as policy expression on any Pinniped Supervisor FederationDomain that includes this IDP. + // + // The configured GitHub App or GitHub OAuth App must be allowed to see membership in the listed organizations, + // otherwise Pinniped will not be aware that the user belongs to the listed organization or any teams + // within that organization. + // + // If no organizations are listed, you must set organizations: AllGitHubUsers. + // + // +kubebuilder:validation:MaxItems=64 + // +listType=set + // +optional + Allowed []string `json:"allowed,omitempty"` +} + +// GitHubAllowAuthenticationSpec allows customization of who can authenticate using this IDP and how. +type GitHubAllowAuthenticationSpec struct { + // Organizations allows customization of which organizations can authenticate using this IDP. + // +kubebuilder:validation:XValidation:message="spec.allowAuthentication.organizations.policy must be 'OnlyUsersFromAllowedOrganizations' when spec.allowAuthentication.organizations.allowed has organizations listed",rule="!(has(self.allowed) && size(self.allowed) > 0 && self.policy == 'AllGitHubUsers')" + // +kubebuilder:validation:XValidation:message="spec.allowAuthentication.organizations.policy must be 'AllGitHubUsers' when spec.allowAuthentication.organizations.allowed is empty",rule="!((!has(self.allowed) || size(self.allowed) == 0) && self.policy == 'OnlyUsersFromAllowedOrganizations')" + Organizations GitHubOrganizationsSpec `json:"organizations"` +} + +// GitHubIdentityProviderSpec is the spec for configuring an GitHub identity provider. +type GitHubIdentityProviderSpec struct { + // GitHubAPI allows configuration for GitHub Enterprise Server + // + // +kubebuilder:default={} + GitHubAPI GitHubAPIConfig `json:"githubAPI,omitempty"` + + // Claims allows customization of the username and groups claims. + // + // +kubebuilder:default={} + Claims GitHubClaims `json:"claims,omitempty"` + + // AllowAuthentication allows customization of who can authenticate using this IDP and how. + AllowAuthentication GitHubAllowAuthenticationSpec `json:"allowAuthentication"` + + // Client identifies the secret with credentials for a GitHub App or GitHub OAuth2 App (a GitHub client). + Client GitHubClientSpec `json:"client"` +} + +// GitHubIdentityProvider describes the configuration of an upstream GitHub identity provider. +// This upstream provider can be configured with either a GitHub App or a GitHub OAuth2 App. +// +// Right now, only web-based logins are supported, for both the pinniped-cli client and clients configured +// as OIDCClients. +// +// +genclient +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object +// +kubebuilder:resource:categories=pinniped;pinniped-idp;pinniped-idps +// +kubebuilder:printcolumn:name="Host",type=string,JSONPath=`.spec.githubAPI.host` +// +kubebuilder:printcolumn:name="Status",type=string,JSONPath=`.status.phase` +// +kubebuilder:printcolumn:name="Age",type=date,JSONPath=`.metadata.creationTimestamp` +// +kubebuilder:subresource:status +type GitHubIdentityProvider struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + // Spec for configuring the identity provider. + Spec GitHubIdentityProviderSpec `json:"spec"` + + // Status of the identity provider. + Status GitHubIdentityProviderStatus `json:"status,omitempty"` +} + +// GitHubIdentityProviderList lists GitHubIdentityProvider objects. +// +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object +type GitHubIdentityProviderList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + + Items []GitHubIdentityProvider `json:"items"` +} diff --git a/generated/latest/apis/supervisor/idp/v1alpha1/types_tls.go b/generated/latest/apis/supervisor/idp/v1alpha1/types_tls.go index 1413a262c..49b49373c 100644 --- a/generated/latest/apis/supervisor/idp/v1alpha1/types_tls.go +++ b/generated/latest/apis/supervisor/idp/v1alpha1/types_tls.go @@ -1,9 +1,9 @@ -// Copyright 2020-2022 the Pinniped contributors. All Rights Reserved. +// Copyright 2020-2024 the Pinniped contributors. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 package v1alpha1 -// Configuration for TLS parameters related to identity provider integration. +// TLSSpec provides TLS configuration for identity provider integration. type TLSSpec struct { // X.509 Certificate Authority (base64-encoded PEM bundle). If omitted, a default set of system roots will be trusted. // +optional diff --git a/generated/latest/apis/supervisor/idp/v1alpha1/zz_generated.deepcopy.go b/generated/latest/apis/supervisor/idp/v1alpha1/zz_generated.deepcopy.go index b0cb168b5..e48860e82 100644 --- a/generated/latest/apis/supervisor/idp/v1alpha1/zz_generated.deepcopy.go +++ b/generated/latest/apis/supervisor/idp/v1alpha1/zz_generated.deepcopy.go @@ -203,6 +203,221 @@ func (in *ActiveDirectoryIdentityProviderUserSearchAttributes) DeepCopy() *Activ return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *GitHubAPIConfig) DeepCopyInto(out *GitHubAPIConfig) { + *out = *in + if in.Host != nil { + in, out := &in.Host, &out.Host + *out = new(string) + **out = **in + } + if in.TLS != nil { + in, out := &in.TLS, &out.TLS + *out = new(TLSSpec) + **out = **in + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GitHubAPIConfig. +func (in *GitHubAPIConfig) DeepCopy() *GitHubAPIConfig { + if in == nil { + return nil + } + out := new(GitHubAPIConfig) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *GitHubAllowAuthenticationSpec) DeepCopyInto(out *GitHubAllowAuthenticationSpec) { + *out = *in + in.Organizations.DeepCopyInto(&out.Organizations) + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GitHubAllowAuthenticationSpec. +func (in *GitHubAllowAuthenticationSpec) DeepCopy() *GitHubAllowAuthenticationSpec { + if in == nil { + return nil + } + out := new(GitHubAllowAuthenticationSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *GitHubClaims) DeepCopyInto(out *GitHubClaims) { + *out = *in + if in.Username != nil { + in, out := &in.Username, &out.Username + *out = new(GitHubUsernameAttribute) + **out = **in + } + if in.Groups != nil { + in, out := &in.Groups, &out.Groups + *out = new(GitHubGroupNameAttribute) + **out = **in + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GitHubClaims. +func (in *GitHubClaims) DeepCopy() *GitHubClaims { + if in == nil { + return nil + } + out := new(GitHubClaims) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *GitHubClientSpec) DeepCopyInto(out *GitHubClientSpec) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GitHubClientSpec. +func (in *GitHubClientSpec) DeepCopy() *GitHubClientSpec { + if in == nil { + return nil + } + out := new(GitHubClientSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *GitHubIdentityProvider) DeepCopyInto(out *GitHubIdentityProvider) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + in.Status.DeepCopyInto(&out.Status) + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GitHubIdentityProvider. +func (in *GitHubIdentityProvider) DeepCopy() *GitHubIdentityProvider { + if in == nil { + return nil + } + out := new(GitHubIdentityProvider) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *GitHubIdentityProvider) 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 *GitHubIdentityProviderList) DeepCopyInto(out *GitHubIdentityProviderList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]GitHubIdentityProvider, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GitHubIdentityProviderList. +func (in *GitHubIdentityProviderList) DeepCopy() *GitHubIdentityProviderList { + if in == nil { + return nil + } + out := new(GitHubIdentityProviderList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *GitHubIdentityProviderList) 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 *GitHubIdentityProviderSpec) DeepCopyInto(out *GitHubIdentityProviderSpec) { + *out = *in + in.GitHubAPI.DeepCopyInto(&out.GitHubAPI) + in.Claims.DeepCopyInto(&out.Claims) + in.AllowAuthentication.DeepCopyInto(&out.AllowAuthentication) + out.Client = in.Client + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GitHubIdentityProviderSpec. +func (in *GitHubIdentityProviderSpec) DeepCopy() *GitHubIdentityProviderSpec { + if in == nil { + return nil + } + out := new(GitHubIdentityProviderSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *GitHubIdentityProviderStatus) DeepCopyInto(out *GitHubIdentityProviderStatus) { + *out = *in + if in.Conditions != nil { + in, out := &in.Conditions, &out.Conditions + *out = make([]v1.Condition, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GitHubIdentityProviderStatus. +func (in *GitHubIdentityProviderStatus) DeepCopy() *GitHubIdentityProviderStatus { + if in == nil { + return nil + } + out := new(GitHubIdentityProviderStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *GitHubOrganizationsSpec) DeepCopyInto(out *GitHubOrganizationsSpec) { + *out = *in + if in.Policy != nil { + in, out := &in.Policy, &out.Policy + *out = new(GitHubAllowedAuthOrganizationsPolicy) + **out = **in + } + if in.Allowed != nil { + in, out := &in.Allowed, &out.Allowed + *out = make([]string, len(*in)) + copy(*out, *in) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GitHubOrganizationsSpec. +func (in *GitHubOrganizationsSpec) DeepCopy() *GitHubOrganizationsSpec { + if in == nil { + return nil + } + out := new(GitHubOrganizationsSpec) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *LDAPIdentityProvider) DeepCopyInto(out *LDAPIdentityProvider) { *out = *in diff --git a/generated/latest/apis/supervisor/idpdiscovery/v1alpha1/types_supervisor_idp_discovery.go b/generated/latest/apis/supervisor/idpdiscovery/v1alpha1/types_supervisor_idp_discovery.go index 837fed2dd..11be6f9dc 100644 --- a/generated/latest/apis/supervisor/idpdiscovery/v1alpha1/types_supervisor_idp_discovery.go +++ b/generated/latest/apis/supervisor/idpdiscovery/v1alpha1/types_supervisor_idp_discovery.go @@ -15,6 +15,7 @@ const ( IDPTypeOIDC IDPType = "oidc" IDPTypeLDAP IDPType = "ldap" IDPTypeActiveDirectory IDPType = "activedirectory" + IDPTypeGitHub IDPType = "github" IDPFlowCLIPassword IDPFlow = "cli_password" IDPFlowBrowserAuthcode IDPFlow = "browser_authcode" diff --git a/generated/latest/client/concierge/openapi/zz_generated.openapi.go b/generated/latest/client/concierge/openapi/zz_generated.openapi.go index ebc44b8e0..acabe99e3 100644 --- a/generated/latest/client/concierge/openapi/zz_generated.openapi.go +++ b/generated/latest/client/concierge/openapi/zz_generated.openapi.go @@ -1875,7 +1875,8 @@ func schema_k8sio_api_core_v1_ConfigMapEnvSource(ref common.ReferenceCallback) c Properties: map[string]spec.Schema{ "name": { SchemaProps: spec.SchemaProps{ - Description: "Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names", + Description: "Name of the referent. This field is effectively required, but due to backwards compatibility is allowed to be empty. Instances of this type with an empty value here are almost certainly wrong. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names", + Default: "", Type: []string{"string"}, Format: "", }, @@ -1902,7 +1903,8 @@ func schema_k8sio_api_core_v1_ConfigMapKeySelector(ref common.ReferenceCallback) Properties: map[string]spec.Schema{ "name": { SchemaProps: spec.SchemaProps{ - Description: "Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names", + Description: "Name of the referent. This field is effectively required, but due to backwards compatibility is allowed to be empty. Instances of this type with an empty value here are almost certainly wrong. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names", + Default: "", Type: []string{"string"}, Format: "", }, @@ -2046,7 +2048,8 @@ func schema_k8sio_api_core_v1_ConfigMapProjection(ref common.ReferenceCallback) Properties: map[string]spec.Schema{ "name": { SchemaProps: spec.SchemaProps{ - Description: "Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names", + Description: "Name of the referent. This field is effectively required, but due to backwards compatibility is allowed to be empty. Instances of this type with an empty value here are almost certainly wrong. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names", + Default: "", Type: []string{"string"}, Format: "", }, @@ -2094,7 +2097,8 @@ func schema_k8sio_api_core_v1_ConfigMapVolumeSource(ref common.ReferenceCallback Properties: map[string]spec.Schema{ "name": { SchemaProps: spec.SchemaProps{ - Description: "Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names", + Description: "Name of the referent. This field is effectively required, but due to backwards compatibility is allowed to be empty. Instances of this type with an empty value here are almost certainly wrong. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names", + Default: "", Type: []string{"string"}, Format: "", }, @@ -4804,6 +4808,7 @@ func schema_k8sio_api_core_v1_HostAlias(ref common.ReferenceCallback) common.Ope "ip": { SchemaProps: spec.SchemaProps{ Description: "IP address of the host file entry.", + Default: "", Type: []string{"string"}, Format: "", }, @@ -4829,6 +4834,7 @@ func schema_k8sio_api_core_v1_HostAlias(ref common.ReferenceCallback) common.Ope }, }, }, + Required: []string{"ip"}, }, }, } @@ -5574,7 +5580,8 @@ func schema_k8sio_api_core_v1_LocalObjectReference(ref common.ReferenceCallback) Properties: map[string]spec.Schema{ "name": { SchemaProps: spec.SchemaProps{ - Description: "Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names", + Description: "Name of the referent. This field is effectively required, but due to backwards compatibility is allowed to be empty. Instances of this type with an empty value here are almost certainly wrong. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names", + Default: "", Type: []string{"string"}, Format: "", }, @@ -11527,7 +11534,8 @@ func schema_k8sio_api_core_v1_SecretEnvSource(ref common.ReferenceCallback) comm Properties: map[string]spec.Schema{ "name": { SchemaProps: spec.SchemaProps{ - Description: "Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names", + Description: "Name of the referent. This field is effectively required, but due to backwards compatibility is allowed to be empty. Instances of this type with an empty value here are almost certainly wrong. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names", + Default: "", Type: []string{"string"}, Format: "", }, @@ -11554,7 +11562,8 @@ func schema_k8sio_api_core_v1_SecretKeySelector(ref common.ReferenceCallback) co Properties: map[string]spec.Schema{ "name": { SchemaProps: spec.SchemaProps{ - Description: "Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names", + Description: "Name of the referent. This field is effectively required, but due to backwards compatibility is allowed to be empty. Instances of this type with an empty value here are almost certainly wrong. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names", + Default: "", Type: []string{"string"}, Format: "", }, @@ -11646,7 +11655,8 @@ func schema_k8sio_api_core_v1_SecretProjection(ref common.ReferenceCallback) com Properties: map[string]spec.Schema{ "name": { SchemaProps: spec.SchemaProps{ - Description: "Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names", + Description: "Name of the referent. This field is effectively required, but due to backwards compatibility is allowed to be empty. Instances of this type with an empty value here are almost certainly wrong. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names", + Default: "", Type: []string{"string"}, Format: "", }, @@ -12502,7 +12512,7 @@ func schema_k8sio_api_core_v1_ServiceSpec(ref common.ReferenceCallback) common.O }, "trafficDistribution": { SchemaProps: spec.SchemaProps{ - Description: "TrafficDistribution offers a way to express preferences for how traffic is distributed to Service endpoints. Implementations can use this field as a hint, but are not required to guarantee strict adherence. If the field is not set, the implementation will apply its default routing strategy. If set to \"PreferClose\", implementations should prioritize endpoints that are topologically close (e.g., same zone).", + Description: "TrafficDistribution offers a way to express preferences for how traffic is distributed to Service endpoints. Implementations can use this field as a hint, but are not required to guarantee strict adherence. If the field is not set, the implementation will apply its default routing strategy. If set to \"PreferClose\", implementations should prioritize endpoints that are topologically close (e.g., same zone). This is an alpha field and requires enabling ServiceTrafficDistribution feature.", Type: []string{"string"}, Format: "", }, diff --git a/generated/latest/client/supervisor/clientset/versioned/typed/idp/v1alpha1/fake/fake_githubidentityprovider.go b/generated/latest/client/supervisor/clientset/versioned/typed/idp/v1alpha1/fake/fake_githubidentityprovider.go new file mode 100644 index 000000000..92a26af6b --- /dev/null +++ b/generated/latest/client/supervisor/clientset/versioned/typed/idp/v1alpha1/fake/fake_githubidentityprovider.go @@ -0,0 +1,128 @@ +// Copyright 2020-2024 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +// Code generated by client-gen. DO NOT EDIT. + +package fake + +import ( + "context" + + v1alpha1 "go.pinniped.dev/generated/latest/apis/supervisor/idp/v1alpha1" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + labels "k8s.io/apimachinery/pkg/labels" + types "k8s.io/apimachinery/pkg/types" + watch "k8s.io/apimachinery/pkg/watch" + testing "k8s.io/client-go/testing" +) + +// FakeGitHubIdentityProviders implements GitHubIdentityProviderInterface +type FakeGitHubIdentityProviders struct { + Fake *FakeIDPV1alpha1 + ns string +} + +var githubidentityprovidersResource = v1alpha1.SchemeGroupVersion.WithResource("githubidentityproviders") + +var githubidentityprovidersKind = v1alpha1.SchemeGroupVersion.WithKind("GitHubIdentityProvider") + +// Get takes name of the gitHubIdentityProvider, and returns the corresponding gitHubIdentityProvider object, and an error if there is any. +func (c *FakeGitHubIdentityProviders) Get(ctx context.Context, name string, options v1.GetOptions) (result *v1alpha1.GitHubIdentityProvider, err error) { + obj, err := c.Fake. + Invokes(testing.NewGetAction(githubidentityprovidersResource, c.ns, name), &v1alpha1.GitHubIdentityProvider{}) + + if obj == nil { + return nil, err + } + return obj.(*v1alpha1.GitHubIdentityProvider), err +} + +// List takes label and field selectors, and returns the list of GitHubIdentityProviders that match those selectors. +func (c *FakeGitHubIdentityProviders) List(ctx context.Context, opts v1.ListOptions) (result *v1alpha1.GitHubIdentityProviderList, err error) { + obj, err := c.Fake. + Invokes(testing.NewListAction(githubidentityprovidersResource, githubidentityprovidersKind, c.ns, opts), &v1alpha1.GitHubIdentityProviderList{}) + + if obj == nil { + return nil, err + } + + label, _, _ := testing.ExtractFromListOptions(opts) + if label == nil { + label = labels.Everything() + } + list := &v1alpha1.GitHubIdentityProviderList{ListMeta: obj.(*v1alpha1.GitHubIdentityProviderList).ListMeta} + for _, item := range obj.(*v1alpha1.GitHubIdentityProviderList).Items { + if label.Matches(labels.Set(item.Labels)) { + list.Items = append(list.Items, item) + } + } + return list, err +} + +// Watch returns a watch.Interface that watches the requested gitHubIdentityProviders. +func (c *FakeGitHubIdentityProviders) Watch(ctx context.Context, opts v1.ListOptions) (watch.Interface, error) { + return c.Fake. + InvokesWatch(testing.NewWatchAction(githubidentityprovidersResource, c.ns, opts)) + +} + +// Create takes the representation of a gitHubIdentityProvider and creates it. Returns the server's representation of the gitHubIdentityProvider, and an error, if there is any. +func (c *FakeGitHubIdentityProviders) Create(ctx context.Context, gitHubIdentityProvider *v1alpha1.GitHubIdentityProvider, opts v1.CreateOptions) (result *v1alpha1.GitHubIdentityProvider, err error) { + obj, err := c.Fake. + Invokes(testing.NewCreateAction(githubidentityprovidersResource, c.ns, gitHubIdentityProvider), &v1alpha1.GitHubIdentityProvider{}) + + if obj == nil { + return nil, err + } + return obj.(*v1alpha1.GitHubIdentityProvider), err +} + +// Update takes the representation of a gitHubIdentityProvider and updates it. Returns the server's representation of the gitHubIdentityProvider, and an error, if there is any. +func (c *FakeGitHubIdentityProviders) Update(ctx context.Context, gitHubIdentityProvider *v1alpha1.GitHubIdentityProvider, opts v1.UpdateOptions) (result *v1alpha1.GitHubIdentityProvider, err error) { + obj, err := c.Fake. + Invokes(testing.NewUpdateAction(githubidentityprovidersResource, c.ns, gitHubIdentityProvider), &v1alpha1.GitHubIdentityProvider{}) + + if obj == nil { + return nil, err + } + return obj.(*v1alpha1.GitHubIdentityProvider), err +} + +// UpdateStatus was generated because the type contains a Status member. +// Add a +genclient:noStatus comment above the type to avoid generating UpdateStatus(). +func (c *FakeGitHubIdentityProviders) UpdateStatus(ctx context.Context, gitHubIdentityProvider *v1alpha1.GitHubIdentityProvider, opts v1.UpdateOptions) (*v1alpha1.GitHubIdentityProvider, error) { + obj, err := c.Fake. + Invokes(testing.NewUpdateSubresourceAction(githubidentityprovidersResource, "status", c.ns, gitHubIdentityProvider), &v1alpha1.GitHubIdentityProvider{}) + + if obj == nil { + return nil, err + } + return obj.(*v1alpha1.GitHubIdentityProvider), err +} + +// Delete takes name of the gitHubIdentityProvider and deletes it. Returns an error if one occurs. +func (c *FakeGitHubIdentityProviders) Delete(ctx context.Context, name string, opts v1.DeleteOptions) error { + _, err := c.Fake. + Invokes(testing.NewDeleteActionWithOptions(githubidentityprovidersResource, c.ns, name, opts), &v1alpha1.GitHubIdentityProvider{}) + + return err +} + +// DeleteCollection deletes a collection of objects. +func (c *FakeGitHubIdentityProviders) DeleteCollection(ctx context.Context, opts v1.DeleteOptions, listOpts v1.ListOptions) error { + action := testing.NewDeleteCollectionAction(githubidentityprovidersResource, c.ns, listOpts) + + _, err := c.Fake.Invokes(action, &v1alpha1.GitHubIdentityProviderList{}) + return err +} + +// Patch applies the patch and returns the patched gitHubIdentityProvider. +func (c *FakeGitHubIdentityProviders) Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts v1.PatchOptions, subresources ...string) (result *v1alpha1.GitHubIdentityProvider, err error) { + obj, err := c.Fake. + Invokes(testing.NewPatchSubresourceAction(githubidentityprovidersResource, c.ns, name, pt, data, subresources...), &v1alpha1.GitHubIdentityProvider{}) + + if obj == nil { + return nil, err + } + return obj.(*v1alpha1.GitHubIdentityProvider), err +} diff --git a/generated/latest/client/supervisor/clientset/versioned/typed/idp/v1alpha1/fake/fake_idp_client.go b/generated/latest/client/supervisor/clientset/versioned/typed/idp/v1alpha1/fake/fake_idp_client.go index 4c12669cb..66ac8e979 100644 --- a/generated/latest/client/supervisor/clientset/versioned/typed/idp/v1alpha1/fake/fake_idp_client.go +++ b/generated/latest/client/supervisor/clientset/versioned/typed/idp/v1alpha1/fake/fake_idp_client.go @@ -19,6 +19,10 @@ func (c *FakeIDPV1alpha1) ActiveDirectoryIdentityProviders(namespace string) v1a return &FakeActiveDirectoryIdentityProviders{c, namespace} } +func (c *FakeIDPV1alpha1) GitHubIdentityProviders(namespace string) v1alpha1.GitHubIdentityProviderInterface { + return &FakeGitHubIdentityProviders{c, namespace} +} + func (c *FakeIDPV1alpha1) LDAPIdentityProviders(namespace string) v1alpha1.LDAPIdentityProviderInterface { return &FakeLDAPIdentityProviders{c, namespace} } diff --git a/generated/latest/client/supervisor/clientset/versioned/typed/idp/v1alpha1/generated_expansion.go b/generated/latest/client/supervisor/clientset/versioned/typed/idp/v1alpha1/generated_expansion.go index 79be098d6..a7acc8120 100644 --- a/generated/latest/client/supervisor/clientset/versioned/typed/idp/v1alpha1/generated_expansion.go +++ b/generated/latest/client/supervisor/clientset/versioned/typed/idp/v1alpha1/generated_expansion.go @@ -7,6 +7,8 @@ package v1alpha1 type ActiveDirectoryIdentityProviderExpansion interface{} +type GitHubIdentityProviderExpansion interface{} + type LDAPIdentityProviderExpansion interface{} type OIDCIdentityProviderExpansion interface{} diff --git a/generated/latest/client/supervisor/clientset/versioned/typed/idp/v1alpha1/githubidentityprovider.go b/generated/latest/client/supervisor/clientset/versioned/typed/idp/v1alpha1/githubidentityprovider.go new file mode 100644 index 000000000..1fdb60270 --- /dev/null +++ b/generated/latest/client/supervisor/clientset/versioned/typed/idp/v1alpha1/githubidentityprovider.go @@ -0,0 +1,182 @@ +// Copyright 2020-2024 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +// Code generated by client-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + "context" + "time" + + v1alpha1 "go.pinniped.dev/generated/latest/apis/supervisor/idp/v1alpha1" + scheme "go.pinniped.dev/generated/latest/client/supervisor/clientset/versioned/scheme" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + types "k8s.io/apimachinery/pkg/types" + watch "k8s.io/apimachinery/pkg/watch" + rest "k8s.io/client-go/rest" +) + +// GitHubIdentityProvidersGetter has a method to return a GitHubIdentityProviderInterface. +// A group's client should implement this interface. +type GitHubIdentityProvidersGetter interface { + GitHubIdentityProviders(namespace string) GitHubIdentityProviderInterface +} + +// GitHubIdentityProviderInterface has methods to work with GitHubIdentityProvider resources. +type GitHubIdentityProviderInterface interface { + Create(ctx context.Context, gitHubIdentityProvider *v1alpha1.GitHubIdentityProvider, opts v1.CreateOptions) (*v1alpha1.GitHubIdentityProvider, error) + Update(ctx context.Context, gitHubIdentityProvider *v1alpha1.GitHubIdentityProvider, opts v1.UpdateOptions) (*v1alpha1.GitHubIdentityProvider, error) + UpdateStatus(ctx context.Context, gitHubIdentityProvider *v1alpha1.GitHubIdentityProvider, opts v1.UpdateOptions) (*v1alpha1.GitHubIdentityProvider, 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) (*v1alpha1.GitHubIdentityProvider, error) + List(ctx context.Context, opts v1.ListOptions) (*v1alpha1.GitHubIdentityProviderList, 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 *v1alpha1.GitHubIdentityProvider, err error) + GitHubIdentityProviderExpansion +} + +// gitHubIdentityProviders implements GitHubIdentityProviderInterface +type gitHubIdentityProviders struct { + client rest.Interface + ns string +} + +// newGitHubIdentityProviders returns a GitHubIdentityProviders +func newGitHubIdentityProviders(c *IDPV1alpha1Client, namespace string) *gitHubIdentityProviders { + return &gitHubIdentityProviders{ + client: c.RESTClient(), + ns: namespace, + } +} + +// Get takes name of the gitHubIdentityProvider, and returns the corresponding gitHubIdentityProvider object, and an error if there is any. +func (c *gitHubIdentityProviders) Get(ctx context.Context, name string, options v1.GetOptions) (result *v1alpha1.GitHubIdentityProvider, err error) { + result = &v1alpha1.GitHubIdentityProvider{} + err = c.client.Get(). + Namespace(c.ns). + Resource("githubidentityproviders"). + Name(name). + VersionedParams(&options, scheme.ParameterCodec). + Do(ctx). + Into(result) + return +} + +// List takes label and field selectors, and returns the list of GitHubIdentityProviders that match those selectors. +func (c *gitHubIdentityProviders) List(ctx context.Context, opts v1.ListOptions) (result *v1alpha1.GitHubIdentityProviderList, err error) { + var timeout time.Duration + if opts.TimeoutSeconds != nil { + timeout = time.Duration(*opts.TimeoutSeconds) * time.Second + } + result = &v1alpha1.GitHubIdentityProviderList{} + err = c.client.Get(). + Namespace(c.ns). + Resource("githubidentityproviders"). + VersionedParams(&opts, scheme.ParameterCodec). + Timeout(timeout). + Do(ctx). + Into(result) + return +} + +// Watch returns a watch.Interface that watches the requested gitHubIdentityProviders. +func (c *gitHubIdentityProviders) Watch(ctx context.Context, opts v1.ListOptions) (watch.Interface, error) { + var timeout time.Duration + if opts.TimeoutSeconds != nil { + timeout = time.Duration(*opts.TimeoutSeconds) * time.Second + } + opts.Watch = true + return c.client.Get(). + Namespace(c.ns). + Resource("githubidentityproviders"). + VersionedParams(&opts, scheme.ParameterCodec). + Timeout(timeout). + Watch(ctx) +} + +// Create takes the representation of a gitHubIdentityProvider and creates it. Returns the server's representation of the gitHubIdentityProvider, and an error, if there is any. +func (c *gitHubIdentityProviders) Create(ctx context.Context, gitHubIdentityProvider *v1alpha1.GitHubIdentityProvider, opts v1.CreateOptions) (result *v1alpha1.GitHubIdentityProvider, err error) { + result = &v1alpha1.GitHubIdentityProvider{} + err = c.client.Post(). + Namespace(c.ns). + Resource("githubidentityproviders"). + VersionedParams(&opts, scheme.ParameterCodec). + Body(gitHubIdentityProvider). + Do(ctx). + Into(result) + return +} + +// Update takes the representation of a gitHubIdentityProvider and updates it. Returns the server's representation of the gitHubIdentityProvider, and an error, if there is any. +func (c *gitHubIdentityProviders) Update(ctx context.Context, gitHubIdentityProvider *v1alpha1.GitHubIdentityProvider, opts v1.UpdateOptions) (result *v1alpha1.GitHubIdentityProvider, err error) { + result = &v1alpha1.GitHubIdentityProvider{} + err = c.client.Put(). + Namespace(c.ns). + Resource("githubidentityproviders"). + Name(gitHubIdentityProvider.Name). + VersionedParams(&opts, scheme.ParameterCodec). + Body(gitHubIdentityProvider). + Do(ctx). + Into(result) + return +} + +// UpdateStatus was generated because the type contains a Status member. +// Add a +genclient:noStatus comment above the type to avoid generating UpdateStatus(). +func (c *gitHubIdentityProviders) UpdateStatus(ctx context.Context, gitHubIdentityProvider *v1alpha1.GitHubIdentityProvider, opts v1.UpdateOptions) (result *v1alpha1.GitHubIdentityProvider, err error) { + result = &v1alpha1.GitHubIdentityProvider{} + err = c.client.Put(). + Namespace(c.ns). + Resource("githubidentityproviders"). + Name(gitHubIdentityProvider.Name). + SubResource("status"). + VersionedParams(&opts, scheme.ParameterCodec). + Body(gitHubIdentityProvider). + Do(ctx). + Into(result) + return +} + +// Delete takes name of the gitHubIdentityProvider and deletes it. Returns an error if one occurs. +func (c *gitHubIdentityProviders) Delete(ctx context.Context, name string, opts v1.DeleteOptions) error { + return c.client.Delete(). + Namespace(c.ns). + Resource("githubidentityproviders"). + Name(name). + Body(&opts). + Do(ctx). + Error() +} + +// DeleteCollection deletes a collection of objects. +func (c *gitHubIdentityProviders) DeleteCollection(ctx context.Context, opts v1.DeleteOptions, listOpts v1.ListOptions) error { + var timeout time.Duration + if listOpts.TimeoutSeconds != nil { + timeout = time.Duration(*listOpts.TimeoutSeconds) * time.Second + } + return c.client.Delete(). + Namespace(c.ns). + Resource("githubidentityproviders"). + VersionedParams(&listOpts, scheme.ParameterCodec). + Timeout(timeout). + Body(&opts). + Do(ctx). + Error() +} + +// Patch applies the patch and returns the patched gitHubIdentityProvider. +func (c *gitHubIdentityProviders) Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts v1.PatchOptions, subresources ...string) (result *v1alpha1.GitHubIdentityProvider, err error) { + result = &v1alpha1.GitHubIdentityProvider{} + err = c.client.Patch(pt). + Namespace(c.ns). + Resource("githubidentityproviders"). + Name(name). + SubResource(subresources...). + VersionedParams(&opts, scheme.ParameterCodec). + Body(data). + Do(ctx). + Into(result) + return +} diff --git a/generated/latest/client/supervisor/clientset/versioned/typed/idp/v1alpha1/idp_client.go b/generated/latest/client/supervisor/clientset/versioned/typed/idp/v1alpha1/idp_client.go index 92f07c525..f97dc2b57 100644 --- a/generated/latest/client/supervisor/clientset/versioned/typed/idp/v1alpha1/idp_client.go +++ b/generated/latest/client/supervisor/clientset/versioned/typed/idp/v1alpha1/idp_client.go @@ -16,6 +16,7 @@ import ( type IDPV1alpha1Interface interface { RESTClient() rest.Interface ActiveDirectoryIdentityProvidersGetter + GitHubIdentityProvidersGetter LDAPIdentityProvidersGetter OIDCIdentityProvidersGetter } @@ -29,6 +30,10 @@ func (c *IDPV1alpha1Client) ActiveDirectoryIdentityProviders(namespace string) A return newActiveDirectoryIdentityProviders(c, namespace) } +func (c *IDPV1alpha1Client) GitHubIdentityProviders(namespace string) GitHubIdentityProviderInterface { + return newGitHubIdentityProviders(c, namespace) +} + func (c *IDPV1alpha1Client) LDAPIdentityProviders(namespace string) LDAPIdentityProviderInterface { return newLDAPIdentityProviders(c, namespace) } diff --git a/generated/latest/client/supervisor/informers/externalversions/generic.go b/generated/latest/client/supervisor/informers/externalversions/generic.go index 1a0ceca9d..ac9a51d09 100644 --- a/generated/latest/client/supervisor/informers/externalversions/generic.go +++ b/generated/latest/client/supervisor/informers/externalversions/generic.go @@ -49,6 +49,8 @@ func (f *sharedInformerFactory) ForResource(resource schema.GroupVersionResource // Group=idp.supervisor.pinniped.dev, Version=v1alpha1 case idpv1alpha1.SchemeGroupVersion.WithResource("activedirectoryidentityproviders"): return &genericInformer{resource: resource.GroupResource(), informer: f.IDP().V1alpha1().ActiveDirectoryIdentityProviders().Informer()}, nil + case idpv1alpha1.SchemeGroupVersion.WithResource("githubidentityproviders"): + return &genericInformer{resource: resource.GroupResource(), informer: f.IDP().V1alpha1().GitHubIdentityProviders().Informer()}, nil case idpv1alpha1.SchemeGroupVersion.WithResource("ldapidentityproviders"): return &genericInformer{resource: resource.GroupResource(), informer: f.IDP().V1alpha1().LDAPIdentityProviders().Informer()}, nil case idpv1alpha1.SchemeGroupVersion.WithResource("oidcidentityproviders"): diff --git a/generated/latest/client/supervisor/informers/externalversions/idp/v1alpha1/githubidentityprovider.go b/generated/latest/client/supervisor/informers/externalversions/idp/v1alpha1/githubidentityprovider.go new file mode 100644 index 000000000..1fed31c5e --- /dev/null +++ b/generated/latest/client/supervisor/informers/externalversions/idp/v1alpha1/githubidentityprovider.go @@ -0,0 +1,77 @@ +// Copyright 2020-2024 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +// Code generated by informer-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + "context" + time "time" + + idpv1alpha1 "go.pinniped.dev/generated/latest/apis/supervisor/idp/v1alpha1" + versioned "go.pinniped.dev/generated/latest/client/supervisor/clientset/versioned" + internalinterfaces "go.pinniped.dev/generated/latest/client/supervisor/informers/externalversions/internalinterfaces" + v1alpha1 "go.pinniped.dev/generated/latest/client/supervisor/listers/idp/v1alpha1" + 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" +) + +// GitHubIdentityProviderInformer provides access to a shared informer and lister for +// GitHubIdentityProviders. +type GitHubIdentityProviderInformer interface { + Informer() cache.SharedIndexInformer + Lister() v1alpha1.GitHubIdentityProviderLister +} + +type gitHubIdentityProviderInformer struct { + factory internalinterfaces.SharedInformerFactory + tweakListOptions internalinterfaces.TweakListOptionsFunc + namespace string +} + +// NewGitHubIdentityProviderInformer constructs a new informer for GitHubIdentityProvider 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 NewGitHubIdentityProviderInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers) cache.SharedIndexInformer { + return NewFilteredGitHubIdentityProviderInformer(client, namespace, resyncPeriod, indexers, nil) +} + +// NewFilteredGitHubIdentityProviderInformer constructs a new informer for GitHubIdentityProvider 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 NewFilteredGitHubIdentityProviderInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers, tweakListOptions internalinterfaces.TweakListOptionsFunc) cache.SharedIndexInformer { + return cache.NewSharedIndexInformer( + &cache.ListWatch{ + ListFunc: func(options v1.ListOptions) (runtime.Object, error) { + if tweakListOptions != nil { + tweakListOptions(&options) + } + return client.IDPV1alpha1().GitHubIdentityProviders(namespace).List(context.TODO(), options) + }, + WatchFunc: func(options v1.ListOptions) (watch.Interface, error) { + if tweakListOptions != nil { + tweakListOptions(&options) + } + return client.IDPV1alpha1().GitHubIdentityProviders(namespace).Watch(context.TODO(), options) + }, + }, + &idpv1alpha1.GitHubIdentityProvider{}, + resyncPeriod, + indexers, + ) +} + +func (f *gitHubIdentityProviderInformer) defaultInformer(client versioned.Interface, resyncPeriod time.Duration) cache.SharedIndexInformer { + return NewFilteredGitHubIdentityProviderInformer(client, f.namespace, resyncPeriod, cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc}, f.tweakListOptions) +} + +func (f *gitHubIdentityProviderInformer) Informer() cache.SharedIndexInformer { + return f.factory.InformerFor(&idpv1alpha1.GitHubIdentityProvider{}, f.defaultInformer) +} + +func (f *gitHubIdentityProviderInformer) Lister() v1alpha1.GitHubIdentityProviderLister { + return v1alpha1.NewGitHubIdentityProviderLister(f.Informer().GetIndexer()) +} diff --git a/generated/latest/client/supervisor/informers/externalversions/idp/v1alpha1/interface.go b/generated/latest/client/supervisor/informers/externalversions/idp/v1alpha1/interface.go index 60166e32e..aad59963b 100644 --- a/generated/latest/client/supervisor/informers/externalversions/idp/v1alpha1/interface.go +++ b/generated/latest/client/supervisor/informers/externalversions/idp/v1alpha1/interface.go @@ -13,6 +13,8 @@ import ( type Interface interface { // ActiveDirectoryIdentityProviders returns a ActiveDirectoryIdentityProviderInformer. ActiveDirectoryIdentityProviders() ActiveDirectoryIdentityProviderInformer + // GitHubIdentityProviders returns a GitHubIdentityProviderInformer. + GitHubIdentityProviders() GitHubIdentityProviderInformer // LDAPIdentityProviders returns a LDAPIdentityProviderInformer. LDAPIdentityProviders() LDAPIdentityProviderInformer // OIDCIdentityProviders returns a OIDCIdentityProviderInformer. @@ -35,6 +37,11 @@ func (v *version) ActiveDirectoryIdentityProviders() ActiveDirectoryIdentityProv return &activeDirectoryIdentityProviderInformer{factory: v.factory, namespace: v.namespace, tweakListOptions: v.tweakListOptions} } +// GitHubIdentityProviders returns a GitHubIdentityProviderInformer. +func (v *version) GitHubIdentityProviders() GitHubIdentityProviderInformer { + return &gitHubIdentityProviderInformer{factory: v.factory, namespace: v.namespace, tweakListOptions: v.tweakListOptions} +} + // LDAPIdentityProviders returns a LDAPIdentityProviderInformer. func (v *version) LDAPIdentityProviders() LDAPIdentityProviderInformer { return &lDAPIdentityProviderInformer{factory: v.factory, namespace: v.namespace, tweakListOptions: v.tweakListOptions} diff --git a/generated/latest/client/supervisor/listers/idp/v1alpha1/expansion_generated.go b/generated/latest/client/supervisor/listers/idp/v1alpha1/expansion_generated.go index b491a9e8d..470c06abb 100644 --- a/generated/latest/client/supervisor/listers/idp/v1alpha1/expansion_generated.go +++ b/generated/latest/client/supervisor/listers/idp/v1alpha1/expansion_generated.go @@ -13,6 +13,14 @@ type ActiveDirectoryIdentityProviderListerExpansion interface{} // ActiveDirectoryIdentityProviderNamespaceLister. type ActiveDirectoryIdentityProviderNamespaceListerExpansion interface{} +// GitHubIdentityProviderListerExpansion allows custom methods to be added to +// GitHubIdentityProviderLister. +type GitHubIdentityProviderListerExpansion interface{} + +// GitHubIdentityProviderNamespaceListerExpansion allows custom methods to be added to +// GitHubIdentityProviderNamespaceLister. +type GitHubIdentityProviderNamespaceListerExpansion interface{} + // LDAPIdentityProviderListerExpansion allows custom methods to be added to // LDAPIdentityProviderLister. type LDAPIdentityProviderListerExpansion interface{} diff --git a/generated/latest/client/supervisor/listers/idp/v1alpha1/githubidentityprovider.go b/generated/latest/client/supervisor/listers/idp/v1alpha1/githubidentityprovider.go new file mode 100644 index 000000000..51564bc9a --- /dev/null +++ b/generated/latest/client/supervisor/listers/idp/v1alpha1/githubidentityprovider.go @@ -0,0 +1,86 @@ +// Copyright 2020-2024 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +// Code generated by lister-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + v1alpha1 "go.pinniped.dev/generated/latest/apis/supervisor/idp/v1alpha1" + "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/client-go/tools/cache" +) + +// GitHubIdentityProviderLister helps list GitHubIdentityProviders. +// All objects returned here must be treated as read-only. +type GitHubIdentityProviderLister interface { + // List lists all GitHubIdentityProviders in the indexer. + // Objects returned here must be treated as read-only. + List(selector labels.Selector) (ret []*v1alpha1.GitHubIdentityProvider, err error) + // GitHubIdentityProviders returns an object that can list and get GitHubIdentityProviders. + GitHubIdentityProviders(namespace string) GitHubIdentityProviderNamespaceLister + GitHubIdentityProviderListerExpansion +} + +// gitHubIdentityProviderLister implements the GitHubIdentityProviderLister interface. +type gitHubIdentityProviderLister struct { + indexer cache.Indexer +} + +// NewGitHubIdentityProviderLister returns a new GitHubIdentityProviderLister. +func NewGitHubIdentityProviderLister(indexer cache.Indexer) GitHubIdentityProviderLister { + return &gitHubIdentityProviderLister{indexer: indexer} +} + +// List lists all GitHubIdentityProviders in the indexer. +func (s *gitHubIdentityProviderLister) List(selector labels.Selector) (ret []*v1alpha1.GitHubIdentityProvider, err error) { + err = cache.ListAll(s.indexer, selector, func(m interface{}) { + ret = append(ret, m.(*v1alpha1.GitHubIdentityProvider)) + }) + return ret, err +} + +// GitHubIdentityProviders returns an object that can list and get GitHubIdentityProviders. +func (s *gitHubIdentityProviderLister) GitHubIdentityProviders(namespace string) GitHubIdentityProviderNamespaceLister { + return gitHubIdentityProviderNamespaceLister{indexer: s.indexer, namespace: namespace} +} + +// GitHubIdentityProviderNamespaceLister helps list and get GitHubIdentityProviders. +// All objects returned here must be treated as read-only. +type GitHubIdentityProviderNamespaceLister interface { + // List lists all GitHubIdentityProviders in the indexer for a given namespace. + // Objects returned here must be treated as read-only. + List(selector labels.Selector) (ret []*v1alpha1.GitHubIdentityProvider, err error) + // Get retrieves the GitHubIdentityProvider from the indexer for a given namespace and name. + // Objects returned here must be treated as read-only. + Get(name string) (*v1alpha1.GitHubIdentityProvider, error) + GitHubIdentityProviderNamespaceListerExpansion +} + +// gitHubIdentityProviderNamespaceLister implements the GitHubIdentityProviderNamespaceLister +// interface. +type gitHubIdentityProviderNamespaceLister struct { + indexer cache.Indexer + namespace string +} + +// List lists all GitHubIdentityProviders in the indexer for a given namespace. +func (s gitHubIdentityProviderNamespaceLister) List(selector labels.Selector) (ret []*v1alpha1.GitHubIdentityProvider, err error) { + err = cache.ListAllByNamespace(s.indexer, s.namespace, selector, func(m interface{}) { + ret = append(ret, m.(*v1alpha1.GitHubIdentityProvider)) + }) + return ret, err +} + +// Get retrieves the GitHubIdentityProvider from the indexer for a given namespace and name. +func (s gitHubIdentityProviderNamespaceLister) Get(name string) (*v1alpha1.GitHubIdentityProvider, error) { + obj, exists, err := s.indexer.GetByKey(s.namespace + "/" + name) + if err != nil { + return nil, err + } + if !exists { + return nil, errors.NewNotFound(v1alpha1.Resource("githubidentityprovider"), name) + } + return obj.(*v1alpha1.GitHubIdentityProvider), nil +} diff --git a/generated/latest/client/supervisor/openapi/zz_generated.openapi.go b/generated/latest/client/supervisor/openapi/zz_generated.openapi.go index ef886d94d..5e4dd25c0 100644 --- a/generated/latest/client/supervisor/openapi/zz_generated.openapi.go +++ b/generated/latest/client/supervisor/openapi/zz_generated.openapi.go @@ -1591,7 +1591,8 @@ func schema_k8sio_api_core_v1_ConfigMapEnvSource(ref common.ReferenceCallback) c Properties: map[string]spec.Schema{ "name": { SchemaProps: spec.SchemaProps{ - Description: "Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names", + Description: "Name of the referent. This field is effectively required, but due to backwards compatibility is allowed to be empty. Instances of this type with an empty value here are almost certainly wrong. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names", + Default: "", Type: []string{"string"}, Format: "", }, @@ -1618,7 +1619,8 @@ func schema_k8sio_api_core_v1_ConfigMapKeySelector(ref common.ReferenceCallback) Properties: map[string]spec.Schema{ "name": { SchemaProps: spec.SchemaProps{ - Description: "Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names", + Description: "Name of the referent. This field is effectively required, but due to backwards compatibility is allowed to be empty. Instances of this type with an empty value here are almost certainly wrong. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names", + Default: "", Type: []string{"string"}, Format: "", }, @@ -1762,7 +1764,8 @@ func schema_k8sio_api_core_v1_ConfigMapProjection(ref common.ReferenceCallback) Properties: map[string]spec.Schema{ "name": { SchemaProps: spec.SchemaProps{ - Description: "Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names", + Description: "Name of the referent. This field is effectively required, but due to backwards compatibility is allowed to be empty. Instances of this type with an empty value here are almost certainly wrong. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names", + Default: "", Type: []string{"string"}, Format: "", }, @@ -1810,7 +1813,8 @@ func schema_k8sio_api_core_v1_ConfigMapVolumeSource(ref common.ReferenceCallback Properties: map[string]spec.Schema{ "name": { SchemaProps: spec.SchemaProps{ - Description: "Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names", + Description: "Name of the referent. This field is effectively required, but due to backwards compatibility is allowed to be empty. Instances of this type with an empty value here are almost certainly wrong. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names", + Default: "", Type: []string{"string"}, Format: "", }, @@ -4520,6 +4524,7 @@ func schema_k8sio_api_core_v1_HostAlias(ref common.ReferenceCallback) common.Ope "ip": { SchemaProps: spec.SchemaProps{ Description: "IP address of the host file entry.", + Default: "", Type: []string{"string"}, Format: "", }, @@ -4545,6 +4550,7 @@ func schema_k8sio_api_core_v1_HostAlias(ref common.ReferenceCallback) common.Ope }, }, }, + Required: []string{"ip"}, }, }, } @@ -5290,7 +5296,8 @@ func schema_k8sio_api_core_v1_LocalObjectReference(ref common.ReferenceCallback) Properties: map[string]spec.Schema{ "name": { SchemaProps: spec.SchemaProps{ - Description: "Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names", + Description: "Name of the referent. This field is effectively required, but due to backwards compatibility is allowed to be empty. Instances of this type with an empty value here are almost certainly wrong. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names", + Default: "", Type: []string{"string"}, Format: "", }, @@ -11243,7 +11250,8 @@ func schema_k8sio_api_core_v1_SecretEnvSource(ref common.ReferenceCallback) comm Properties: map[string]spec.Schema{ "name": { SchemaProps: spec.SchemaProps{ - Description: "Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names", + Description: "Name of the referent. This field is effectively required, but due to backwards compatibility is allowed to be empty. Instances of this type with an empty value here are almost certainly wrong. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names", + Default: "", Type: []string{"string"}, Format: "", }, @@ -11270,7 +11278,8 @@ func schema_k8sio_api_core_v1_SecretKeySelector(ref common.ReferenceCallback) co Properties: map[string]spec.Schema{ "name": { SchemaProps: spec.SchemaProps{ - Description: "Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names", + Description: "Name of the referent. This field is effectively required, but due to backwards compatibility is allowed to be empty. Instances of this type with an empty value here are almost certainly wrong. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names", + Default: "", Type: []string{"string"}, Format: "", }, @@ -11362,7 +11371,8 @@ func schema_k8sio_api_core_v1_SecretProjection(ref common.ReferenceCallback) com Properties: map[string]spec.Schema{ "name": { SchemaProps: spec.SchemaProps{ - Description: "Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names", + Description: "Name of the referent. This field is effectively required, but due to backwards compatibility is allowed to be empty. Instances of this type with an empty value here are almost certainly wrong. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names", + Default: "", Type: []string{"string"}, Format: "", }, @@ -12218,7 +12228,7 @@ func schema_k8sio_api_core_v1_ServiceSpec(ref common.ReferenceCallback) common.O }, "trafficDistribution": { SchemaProps: spec.SchemaProps{ - Description: "TrafficDistribution offers a way to express preferences for how traffic is distributed to Service endpoints. Implementations can use this field as a hint, but are not required to guarantee strict adherence. If the field is not set, the implementation will apply its default routing strategy. If set to \"PreferClose\", implementations should prioritize endpoints that are topologically close (e.g., same zone).", + Description: "TrafficDistribution offers a way to express preferences for how traffic is distributed to Service endpoints. Implementations can use this field as a hint, but are not required to guarantee strict adherence. If the field is not set, the implementation will apply its default routing strategy. If set to \"PreferClose\", implementations should prioritize endpoints that are topologically close (e.g., same zone). This is an alpha field and requires enabling ServiceTrafficDistribution feature.", Type: []string{"string"}, Format: "", }, diff --git a/go.mod b/go.mod index b2fce0c7d..93f0ef798 100644 --- a/go.mod +++ b/go.mod @@ -2,7 +2,7 @@ module go.pinniped.dev go 1.22.0 -toolchain go1.22.2 +toolchain go1.22.4 // This version taken from https://github.com/kubernetes/apiserver/blob/v0.30.0/go.mod#L14 to avoid compile failures. replace github.com/google/cel-go => github.com/google/cel-go v0.17.8 @@ -32,7 +32,7 @@ replace github.com/coreos/go-oidc/v3 => github.com/coreos/go-oidc/v3 v3.9.0 require ( github.com/MakeNowJust/heredoc/v2 v2.0.1 - github.com/chromedp/cdproto v0.0.0-20240519224452-66462be74baa + github.com/chromedp/cdproto v0.0.0-20240602235142-49d0e97b7881 github.com/chromedp/chromedp v0.9.5 github.com/coreos/go-oidc/v3 v3.10.0 github.com/coreos/go-semver v0.3.1 @@ -41,34 +41,36 @@ require ( github.com/felixge/httpsnoop v1.0.4 github.com/go-jose/go-jose/v3 v3.0.3 github.com/go-ldap/ldap/v3 v3.4.8 - github.com/go-logr/logr v1.4.1 + github.com/go-logr/logr v1.4.2 github.com/go-logr/stdr v1.2.2 github.com/go-logr/zapr v1.3.0 github.com/gofrs/flock v0.8.1 github.com/google/cel-go v0.20.1 github.com/google/go-cmp v0.6.0 + github.com/google/go-github/v62 v62.0.0 github.com/google/gofuzz v1.2.0 github.com/google/uuid v1.6.0 github.com/gorilla/securecookie v1.1.2 - github.com/gorilla/websocket v1.5.1 + github.com/gorilla/websocket v1.5.2 github.com/joshlf/go-acl v0.0.0-20200411065538-eae00ae38531 + github.com/migueleliasweb/go-github-mock v0.0.23 github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 - github.com/ory/fosite v0.46.2-0.20240403135905-5e039ca9eef1 + github.com/ory/fosite v0.46.2-0.20240522073333-1e7c582e74e4 github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c github.com/pkg/errors v0.9.1 github.com/sclevine/spec v1.4.0 github.com/spf13/cobra v1.8.0 github.com/spf13/pflag v1.0.5 github.com/stretchr/testify v1.9.0 - github.com/tdewolff/minify/v2 v2.20.25 + github.com/tdewolff/minify/v2 v2.20.33 go.uber.org/mock v0.4.0 go.uber.org/zap v1.27.0 - golang.org/x/crypto v0.23.0 - golang.org/x/net v0.25.0 - golang.org/x/oauth2 v0.20.0 + golang.org/x/crypto v0.24.0 + golang.org/x/net v0.26.0 + golang.org/x/oauth2 v0.21.0 golang.org/x/sync v0.7.0 - golang.org/x/term v0.20.0 - golang.org/x/text v0.15.0 + golang.org/x/term v0.21.0 + golang.org/x/text v0.16.0 k8s.io/api v0.30.1 k8s.io/apiextensions-apiserver v0.30.1 k8s.io/apimachinery v0.30.1 @@ -78,7 +80,7 @@ require ( k8s.io/gengo v0.0.0-20240404160639-a0386bf69313 k8s.io/klog/v2 v2.120.1 k8s.io/kube-aggregator v0.30.1 - k8s.io/kube-openapi v0.0.0-20240521025948-451ce29f5b89 + k8s.io/kube-openapi v0.0.0-20240521193020-835d969ad83a k8s.io/utils v0.0.0-20240502163921-fe8a2dddb1d0 sigs.k8s.io/yaml v1.4.0 ) @@ -120,6 +122,9 @@ require ( github.com/golang/mock v1.6.0 // indirect github.com/golang/protobuf v1.5.4 // indirect github.com/google/gnostic-models v0.6.8 // indirect + github.com/google/go-github/v59 v59.0.0 // indirect + github.com/google/go-querystring v1.1.0 // indirect + github.com/gorilla/mux v1.8.0 // indirect github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.2 // indirect github.com/hashicorp/go-cleanhttp v0.5.2 // indirect @@ -183,9 +188,9 @@ require ( go.uber.org/multierr v1.11.0 // indirect golang.org/x/exp v0.0.0-20230515195305-f3d0a9c9a5cc // indirect golang.org/x/mod v0.17.0 // indirect - golang.org/x/sys v0.20.0 // indirect + golang.org/x/sys v0.21.0 // indirect golang.org/x/time v0.3.0 // indirect - golang.org/x/tools v0.20.0 // indirect + golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect google.golang.org/genproto v0.0.0-20230822172742-b8732ec3820d // indirect google.golang.org/genproto/googleapis/api v0.0.0-20230822172742-b8732ec3820d // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20230822172742-b8732ec3820d // indirect diff --git a/go.sum b/go.sum index 3a2438d15..d821350ba 100644 --- a/go.sum +++ b/go.sum @@ -69,8 +69,8 @@ github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XL github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/chromedp/cdproto v0.0.0-20240202021202-6d0b6a386732/go.mod h1:GKljq0VrfU4D5yc+2qA6OVr8pmO/MBbPEWqWQ/oqGEs= -github.com/chromedp/cdproto v0.0.0-20240519224452-66462be74baa h1:T3Ho4BWIkoEoMPCj90W2HIPF/k56qk4JWzTs6JUBxVw= -github.com/chromedp/cdproto v0.0.0-20240519224452-66462be74baa/go.mod h1:GKljq0VrfU4D5yc+2qA6OVr8pmO/MBbPEWqWQ/oqGEs= +github.com/chromedp/cdproto v0.0.0-20240602235142-49d0e97b7881 h1:RAUqkPvbEDGPgCYVc4GefBqAorWJAjKpVHgsRZyJmGE= +github.com/chromedp/cdproto v0.0.0-20240602235142-49d0e97b7881/go.mod h1:GKljq0VrfU4D5yc+2qA6OVr8pmO/MBbPEWqWQ/oqGEs= github.com/chromedp/chromedp v0.9.5 h1:viASzruPJOiThk7c5bueOUY91jGLJVximoEMGoH93rg= github.com/chromedp/chromedp v0.9.5/go.mod h1:D4I2qONslauw/C7INoCir1BJkSwBYMyZgx8X276z3+Y= github.com/chromedp/sysutil v1.0.0 h1:+ZxhTpfpZlmchB58ih/LBHX52ky7w2VhQVKQMucy3Ic= @@ -156,8 +156,8 @@ github.com/go-ldap/ldap/v3 v3.4.8/go.mod h1:qS3Sjlu76eHfHGpUdWkAXQTw4beih+cHsco2 github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= github.com/go-logr/logr v0.2.0/go.mod h1:z6/tIYblkpsD+a4lm/fGIIU9mZ+XfAiaFtq7xTgseGU= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= -github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ= -github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= +github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-logr/zapr v1.3.0 h1:XGdV8XW8zdwFiwOA2Dryh1gj2KRQyOOoNmBy4EplIcQ= @@ -257,6 +257,12 @@ github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-github/v59 v59.0.0 h1:7h6bgpF5as0YQLLkEiVqpgtJqjimMYhBkD4jT5aN3VA= +github.com/google/go-github/v59 v59.0.0/go.mod h1:rJU4R0rQHFVFDOkqGWxfLNo6vEk4dv40oDjhV/gH6wM= +github.com/google/go-github/v62 v62.0.0 h1:/6mGCaRywZz9MuHyw9gD1CwsbmBX8GWsbFkwMmHdhl4= +github.com/google/go-github/v62 v62.0.0/go.mod h1:EMxeUqGJq2xRu9DYBMwel/mr7kZrzUOfQmmpYrZn2a4= +github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= +github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.1.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= @@ -284,13 +290,15 @@ github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+ github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g= github.com/gorilla/css v1.0.0/go.mod h1:Dn721qIggHpt4+EFCcTLTU/vk5ySda2ReITrtgBl60c= +github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= +github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4= github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA= github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo= github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM= github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= -github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY= -github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY= +github.com/gorilla/websocket v1.5.2 h1:qoW6V1GT3aZxybsbC6oLnailWnB+qTMVwMreOso9XUw= +github.com/gorilla/websocket v1.5.2/go.mod h1:0n9H61RBAcf5/38py2MCYbxzPIY9rOkpvvMT24Rqs30= github.com/grpc-ecosystem/go-grpc-middleware v1.3.0 h1:+9834+KizmvFV7pXQGSXQTsaWhq2GjuNUt0aUU0YBYw= github.com/grpc-ecosystem/go-grpc-middleware v1.3.0/go.mod h1:z0ButlSOZa5vEBq9m2m2hlwIgKw+rp3sdCBRoJY+30Y= github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 h1:Ovs26xHkKqVztRpIrF/92BcuyuQ/YW4NSIpoGtfXNho= @@ -440,6 +448,8 @@ github.com/mattn/goveralls v0.0.12/go.mod h1:44ImGEUfmqH8bBtaMrYKsM65LXfNLWmwaxF github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo= github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= github.com/microcosm-cc/bluemonday v1.0.20/go.mod h1:yfBmMi8mxvaZut3Yytv+jTXRY8mxyjJ0/kQBTElld50= +github.com/migueleliasweb/go-github-mock v0.0.23 h1:GOi9oX/+Seu9JQ19V8bPDLqDI7M9iEOjo3g8v1k6L2c= +github.com/migueleliasweb/go-github-mock v0.0.23/go.mod h1:NsT8FGbkvIZQtDu38+295sZEX8snaUiiQgsGxi6GUxk= github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= @@ -472,8 +482,8 @@ github.com/openzipkin/zipkin-go v0.4.1 h1:kNd/ST2yLLWhaWrkgchya40TJabe8Hioj9udfP github.com/openzipkin/zipkin-go v0.4.1/go.mod h1:qY0VqDSN1pOBN94dBc6w2GJlWLiovAyg7Qt6/I9HecM= github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde h1:x0TT0RDC7UhAVbbWWBzr41ElhJx5tXPWkIHA2HWPRuw= github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde/go.mod h1:nZgzbfBr3hhjoZnS66nKrHmduYNpc34ny7RK4z5/HM0= -github.com/ory/fosite v0.46.2-0.20240403135905-5e039ca9eef1 h1:Ev2BRtVe54kwAQ0dEEmdJIZHbJSpQdpOluEdrPae+sM= -github.com/ory/fosite v0.46.2-0.20240403135905-5e039ca9eef1/go.mod h1:1L248mlkShpxI2qi2RABiEtf86jFH414HvAERTpgEWM= +github.com/ory/fosite v0.46.2-0.20240522073333-1e7c582e74e4 h1:TsV20nTmjLt6uNe2FZQJ9c4CZBcD5xxu1g8q7jW8Yfc= +github.com/ory/fosite v0.46.2-0.20240522073333-1e7c582e74e4/go.mod h1:1L248mlkShpxI2qi2RABiEtf86jFH414HvAERTpgEWM= github.com/ory/go-acc v0.2.9-0.20230103102148-6b1c9a70dbbe h1:rvu4obdvqR0fkSIJ8IfgzKOWwZ5kOT2UNfLq81Qk7rc= github.com/ory/go-acc v0.2.9-0.20230103102148-6b1c9a70dbbe/go.mod h1:z4n3u6as84LbV4YmgjHhnwtccQqzf4cZlSk9f1FhygI= github.com/ory/go-convenience v0.1.0 h1:zouLKfF2GoSGnJwGq+PE/nJAE6dj2Zj5QlTgmMTsTS8= @@ -567,8 +577,8 @@ github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsT github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/subosito/gotenv v1.4.2 h1:X1TuBLAMDFbaTAChgCBLu3DU3UPyELpnF2jjJ2cz/S8= github.com/subosito/gotenv v1.4.2/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0= -github.com/tdewolff/minify/v2 v2.20.25 h1:eDEMiVTnLNHPYV3+pbUHR7nJ5Y3Vlee0vV7JVVK0Ekc= -github.com/tdewolff/minify/v2 v2.20.25/go.mod h1:1TJni7+mATKu24cBQQpgwakrYRD27uC1/rdJOgdv8ns= +github.com/tdewolff/minify/v2 v2.20.33 h1:lZFesDQagd+zGxyC3fEO/X2jZWB8CrahKi77lNrgAAQ= +github.com/tdewolff/minify/v2 v2.20.33/go.mod h1:1TJni7+mATKu24cBQQpgwakrYRD27uC1/rdJOgdv8ns= github.com/tdewolff/parse/v2 v2.7.14 h1:100KJ+QAO3PpMb3uUjzEU/NpmCdbBYz6KPmCIAfWpR8= github.com/tdewolff/parse/v2 v2.7.14/go.mod h1:3FbJWZp3XT9OWVN3Hmfp0p/a08v4h8J9W1aghka0soA= github.com/tdewolff/test v1.0.11-0.20231101010635-f1265d231d52/go.mod h1:6DAvZliBAAnD7rhVgwaM7DE5/d9NMOAJ09SqYqeK4QE= @@ -684,8 +694,8 @@ golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0 golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58= golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= -golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI= -golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= +golang.org/x/crypto v0.24.0 h1:mnl8DM0o513X8fdIkmyFE/5hTYxbwYOjDS/+rK6qpRI= +golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= @@ -770,8 +780,8 @@ golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= -golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac= -golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= +golang.org/x/net v0.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ= +golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -781,8 +791,8 @@ golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.20.0 h1:4mQdhULixXKP1rwYBW0vAijoXnkTG0BLCDRzfe1idMo= -golang.org/x/oauth2 v0.20.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= +golang.org/x/oauth2 v0.21.0 h1:tsimM75w1tF/uws5rbeHzIWxEqElMehnc+iW793zsZs= +golang.org/x/oauth2 v0.21.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -857,8 +867,8 @@ golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y= -golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws= +golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= 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= @@ -868,8 +878,8 @@ golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY= 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.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58= -golang.org/x/term v0.20.0 h1:VnkxpohqXaOBYJtBmEppKUG6mXpi+4O6purfc2+sMhw= -golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= +golang.org/x/term v0.21.0 h1:WVXCp+/EBEHOj53Rvu+7KiT/iElMrO8ACK16SMZ3jaA= +golang.org/x/term v0.21.0/go.mod h1:ooXLefLobQVslOqselCNF4SxFAaoS6KujMbsGzSDmX0= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -881,8 +891,8 @@ 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.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk= -golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= +golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= @@ -947,8 +957,8 @@ golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= 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.8.0/go.mod h1:JxBZ99ISMI5ViVkT1tr6tdNmXeTrcpVSD3vZ1RsRdN4= -golang.org/x/tools v0.20.0 h1:hz/CVckiOxybQvFw6h7b/q80NTr9IUQb4s1IIzW7KNY= -golang.org/x/tools v0.20.0/go.mod h1:WvitBU7JJf6A4jOdg4S1tviW9bhUxkgeCui/0JHctQg= +golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg= +golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -1105,8 +1115,8 @@ k8s.io/kms v0.30.1 h1:gEIbEeCbFiaN2tNfp/EUhFdGr5/CSj8Eyq6Mkr7cCiY= k8s.io/kms v0.30.1/go.mod h1:GrMurD0qk3G4yNgGcsCEmepqf9KyyIrTXYR2lyUOJC4= k8s.io/kube-aggregator v0.30.1 h1:ymR2BsxDacTKwzKTuNhGZttuk009c+oZbSeD+IPX5q4= k8s.io/kube-aggregator v0.30.1/go.mod h1:SFbqWsM6ea8dHd3mPLsZFzJHbjBOS5ykIgJh4znZ5iQ= -k8s.io/kube-openapi v0.0.0-20240521025948-451ce29f5b89 h1:PVDt+zAAka/NPJmeBw9xmTwbMKVVAcB2wYGOHrbWKdA= -k8s.io/kube-openapi v0.0.0-20240521025948-451ce29f5b89/go.mod h1:PMabYkVfJJ5KPe2D98XW9A3kZYKnxJnBRsKLWIPyFv0= +k8s.io/kube-openapi v0.0.0-20240521193020-835d969ad83a h1:zD1uj3Jf+mD4zmA7W+goE5TxDkI7OGJjBNBzq5fJtLA= +k8s.io/kube-openapi v0.0.0-20240521193020-835d969ad83a/go.mod h1:UxDHUPsUwTOOxSU+oXURfFBcAS6JwiRXTYqYwfuGowc= k8s.io/utils v0.0.0-20240502163921-fe8a2dddb1d0 h1:jgGTlFYnhF1PM1Ax/lAlxUPE+KfCIXHaathvJg1C3ak= k8s.io/utils v0.0.0-20240502163921-fe8a2dddb1d0/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= diff --git a/hack/Dockerfile_fips b/hack/Dockerfile_fips index 3996b380d..7b423ad47 100644 --- a/hack/Dockerfile_fips +++ b/hack/Dockerfile_fips @@ -1,6 +1,6 @@ # syntax=docker/dockerfile:1 -# Copyright 2022-2023 the Pinniped contributors. All Rights Reserved. +# Copyright 2022-2024 the Pinniped contributors. All Rights Reserved. # SPDX-License-Identifier: Apache-2.0 # this dockerfile is used to produce a binary of Pinniped that uses @@ -16,7 +16,7 @@ # See https://go.googlesource.com/go/+/dev.boringcrypto/README.boringcrypto.md # and https://kupczynski.info/posts/fips-golang/ for details. -ARG BUILD_IMAGE=golang:1.22.3@sha256:f43c6f049f04cbbaeb28f0aad3eea15274a7d0a7899a617d0037aec48d7ab010 +ARG BUILD_IMAGE=golang:1.22.4@sha256:969349b8121a56d51c74f4c273ab974c15b3a8ae246a5cffc1df7d28b66cf978 ARG BASE_IMAGE=gcr.io/distroless/static:nonroot@sha256:e9ac71e2b8e279a8372741b7a0293afda17650d926900233ec3a7b2b7c22a246 # This is not currently using --platform to prepare to cross-compile because we use gcc below to build diff --git a/hack/lib/kube-versions.txt b/hack/lib/kube-versions.txt index a7872f7f3..7793b644f 100644 --- a/hack/lib/kube-versions.txt +++ b/hack/lib/kube-versions.txt @@ -11,10 +11,10 @@ # # Whenever a new version is added to this file, run hack/update.sh. # -1.30.0 -1.29.4 -1.28.9 -1.27.13 +1.30.1 +1.29.5 +1.28.10 +1.27.14 1.26.15 1.25.16 1.24.17 diff --git a/hack/prepare-for-integration-tests.sh b/hack/prepare-for-integration-tests.sh index 7159b9a15..24a6acec4 100755 --- a/hack/prepare-for-integration-tests.sh +++ b/hack/prepare-for-integration-tests.sh @@ -1,6 +1,6 @@ #!/usr/bin/env bash -# Copyright 2020-2023 the Pinniped contributors. All Rights Reserved. +# Copyright 2020-2024 the Pinniped contributors. All Rights Reserved. # SPDX-License-Identifier: Apache-2.0 # @@ -31,6 +31,7 @@ clean_kind=no api_group_suffix="pinniped.dev" # same default as in the values.yaml ytt file dockerfile_path="" get_active_directory_vars="" # specify a filename for a script to get AD related env variables +get_github_vars="" # specify a filename for a script to get GitHub related env variables alternate_deploy="undefined" pre_install="undefined" @@ -68,6 +69,16 @@ while (("$#")); do get_active_directory_vars=$1 shift ;; + --get-github-vars) + shift + # If there are no more command line arguments, or there is another command line argument but it starts with a dash, then error + if [[ "$#" == "0" || "$1" == -* ]]; then + log_error "--get-github-vars requires a script name to be specified" + exit 1 + fi + get_github_vars=$1 + shift + ;; --dockerfile-path) shift # If there are no more command line arguments, or there is another command line argument but it starts with a dash, then error @@ -121,6 +132,7 @@ if [[ "$help" == "yes" ]]; then log_note " -g, --api-group-suffix: deploy Pinniped with an alternate API group suffix" log_note " -s, --skip-build: reuse the most recently built image of the app instead of building" log_note " -a, --get-active-directory-vars: specify a script that exports active directory environment variables" + log_note " --get-github-vars: specify a script that exports GitHub environment variables" log_note " --alternate-deploy: specify an alternate deploy script to install all components of Pinniped" log_note " --pre-install: specify an pre-install script such as a build script" exit 1 @@ -453,6 +465,15 @@ if [[ "$get_active_directory_vars" != "" ]]; then source $get_active_directory_vars fi +# We can't set up an in-cluster GitHub instance, but +# if you have a GitHub account that you wish to run the tests against, +# specify a script to set the GitHub environment variables. +# You will need to set the environment variables that start with "PINNIPED_TEST_GITHUB_" +# found in pinniped/test/testlib/env.go. +if [[ "$get_github_vars" != "" ]]; then + source $get_github_vars +fi + read -r -d '' PINNIPED_TEST_CLUSTER_CAPABILITY_YAML << PINNIPED_TEST_CLUSTER_CAPABILITY_YAML_EOF || true ${pinniped_cluster_capability_file_content} PINNIPED_TEST_CLUSTER_CAPABILITY_YAML_EOF diff --git a/hack/prepare-supervisor-on-kind.sh b/hack/prepare-supervisor-on-kind.sh index 147a1869d..0bd24c43f 100755 --- a/hack/prepare-supervisor-on-kind.sh +++ b/hack/prepare-supervisor-on-kind.sh @@ -1,6 +1,6 @@ #!/usr/bin/env bash -# Copyright 2021-2023 the Pinniped contributors. All Rights Reserved. +# Copyright 2021-2024 the Pinniped contributors. All Rights Reserved. # SPDX-License-Identifier: Apache-2.0 # @@ -42,6 +42,7 @@ source hack/lib/helpers.sh use_oidc_upstream=no use_ldap_upstream=no use_ad_upstream=no +use_github_upstream=no use_flow="" while (("$#")); do case "$1" in @@ -67,6 +68,10 @@ while (("$#")); do use_oidc_upstream=yes shift ;; + --github) + use_github_upstream=yes + shift + ;; --ad) # Use an ActiveDirectoryIdentityProvider. # This assumes that you used the --get-active-directory-vars flag with hack/prepare-for-integration-tests.sh. @@ -84,8 +89,8 @@ while (("$#")); do esac done -if [[ "$use_oidc_upstream" == "no" && "$use_ldap_upstream" == "no" && "$use_ad_upstream" == "no" ]]; then - log_error "Error: Please use --oidc, --ldap, or --ad to specify which type(s) of upstream identity provider(s) you would like. May use one or multiple." +if [[ "$use_oidc_upstream" == "no" && "$use_ldap_upstream" == "no" && "$use_ad_upstream" == "no" && "$use_github_upstream" == "no" ]]; then + log_error "Error: Please use --oidc, --ldap, --ad, or --github to specify which type(s) of upstream identity provider(s) you would like. May use one or multiple." exit 1 fi @@ -290,6 +295,39 @@ EOF --dry-run=client --output yaml | kubectl apply -f - fi +if [[ "$use_github_upstream" == "yes" ]]; then + # Make an GitHubIdentityProvider. Needs to be configured with an actual GitHub App or GitHub OAuth App. + cat <>$fd_file - - displayName: "My AD IDP" + - displayName: "My AD IDP 🚀" objectRef: apiGroup: idp.supervisor.pinniped.dev kind: ActiveDirectoryIdentityProvider @@ -414,6 +452,18 @@ if [[ "$use_ad_upstream" == "yes" ]]; then EOF fi +if [[ "$use_github_upstream" == "yes" ]]; then + # Indenting the heredoc by 4 spaces to make it indented the correct amount in the FederationDomain below. + cat <>$fd_file + + - displayName: "My GitHub IDP 🚀" + objectRef: + apiGroup: idp.supervisor.pinniped.dev + kind: GitHubIdentityProvider + name: my-github-provider +EOF +fi + # Apply the FederationDomain from the file created above. kubectl apply --namespace "$PINNIPED_TEST_SUPERVISOR_NAMESPACE" -f "$fd_file" @@ -496,6 +546,11 @@ if [[ "$use_ad_upstream" == "yes" ]]; then https_proxy="$proxy_server" no_proxy="$proxy_except" \ ./pinniped get kubeconfig --oidc-skip-browser $flow_arg --upstream-identity-provider-type activedirectory >kubeconfig-ad.yaml fi +if [[ "$use_github_upstream" == "yes" ]]; then + echo "Generating GitHub kubeconfig..." + https_proxy="$proxy_server" no_proxy="$proxy_except" \ + ./pinniped get kubeconfig --oidc-skip-browser $flow_arg --upstream-identity-provider-type github >kubeconfig-github.yaml +fi # Clear the local CLI cache to ensure that the kubectl command below will need to perform a fresh login. rm -f "$HOME/.config/pinniped/sessions.yaml" @@ -534,6 +589,12 @@ if [[ "$use_ad_upstream" == "yes" ]]; then echo fi +if [[ "$use_github_upstream" == "yes" ]]; then + echo " GitHub Username: $PINNIPED_TEST_GITHUB_USER_USERNAME (or use your own account)" + echo " GitHub Password: $PINNIPED_TEST_GITHUB_USER_PASSWORD (also requires OTP, or use your own account)" + echo +fi + # Echo the commands that may be used to login and print the identity of the currently logged in user. # Once the CLI has cached your tokens, it will automatically refresh your short-lived credentials whenever # they expire, so you should not be prompted to log in again for the rest of the day. @@ -552,3 +613,8 @@ if [[ "$use_ad_upstream" == "yes" ]]; then echo "PINNIPED_DEBUG=true ${proxy_env_vars}./pinniped whoami --kubeconfig ./kubeconfig-ad.yaml" echo fi +if [[ "$use_github_upstream" == "yes" ]]; then + echo "To log in using GitHub, run:" + echo "PINNIPED_DEBUG=true ${proxy_env_vars}./pinniped whoami --kubeconfig ./kubeconfig-github.yaml" + echo +fi diff --git a/hack/update-go-mod/go.mod b/hack/update-go-mod/go.mod index 7df7b9863..f7bc8facc 100644 --- a/hack/update-go-mod/go.mod +++ b/hack/update-go-mod/go.mod @@ -4,4 +4,4 @@ go 1.22.0 toolchain go1.22.2 -require golang.org/x/mod v0.17.0 +require golang.org/x/mod v0.18.0 diff --git a/hack/update-go-mod/go.sum b/hack/update-go-mod/go.sum index e69cc3746..abd6dbdee 100644 --- a/hack/update-go-mod/go.sum +++ b/hack/update-go-mod/go.sum @@ -1,2 +1,2 @@ -golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA= -golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/mod v0.18.0 h1:5+9lSbEzPSdWkH32vYPBwEpX8KwDbM52Ud9xBUvNlb0= +golang.org/x/mod v0.18.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= diff --git a/internal/controller/authenticator/authenticator.go b/internal/controller/authenticator/authenticator.go index 59b1cc956..cab5e97b4 100644 --- a/internal/controller/authenticator/authenticator.go +++ b/internal/controller/authenticator/authenticator.go @@ -4,16 +4,6 @@ // Package authenticator contains helper code for dealing with *Authenticator CRDs. package authenticator -import ( - "crypto/x509" - "encoding/base64" - "fmt" - - "k8s.io/client-go/util/cert" - - authenticationv1alpha1 "go.pinniped.dev/generated/latest/apis/concierge/authentication/v1alpha1" -) - // Closer is a type that can be closed idempotently. // // This type is slightly different from io.Closer, because io.Closer can return an error and is not @@ -21,24 +11,3 @@ import ( type Closer interface { Close() } - -// CABundle returns a PEM-encoded CA bundle from the provided spec. If the provided spec is nil, a -// nil CA bundle will be returned. If the provided spec contains a CA bundle that is not properly -// encoded, an error will be returned. -func CABundle(spec *authenticationv1alpha1.TLSSpec) (*x509.CertPool, []byte, error) { - if spec == nil || len(spec.CertificateAuthorityData) == 0 { - return nil, nil, nil - } - - pem, err := base64.StdEncoding.DecodeString(spec.CertificateAuthorityData) - if err != nil { - return nil, nil, err - } - - rootCAs, err := cert.NewPoolFromBytes(pem) - if err != nil { - return nil, nil, fmt.Errorf("certificateAuthorityData is not valid PEM: %w", err) - } - - return rootCAs, pem, nil -} diff --git a/internal/controller/authenticator/jwtcachefiller/jwtcachefiller.go b/internal/controller/authenticator/jwtcachefiller/jwtcachefiller.go index 02b8289a9..529798d1e 100644 --- a/internal/controller/authenticator/jwtcachefiller/jwtcachefiller.go +++ b/internal/controller/authenticator/jwtcachefiller/jwtcachefiller.go @@ -246,7 +246,7 @@ func (c *jwtCacheFillerController) extractValueAsJWTAuthenticator(value authncac } func (c *jwtCacheFillerController) validateTLS(tlsSpec *authenticationv1alpha1.TLSSpec, conditions []*metav1.Condition) (*x509.CertPool, []*metav1.Condition, bool) { - rootCAs, _, err := pinnipedauthenticator.CABundle(tlsSpec) + rootCAs, _, err := pinnipedcontroller.BuildCertPoolAuth(tlsSpec) if err != nil { msg := fmt.Sprintf("%s: %s", "invalid TLS configuration", err.Error()) conditions = append(conditions, &metav1.Condition{ @@ -603,7 +603,7 @@ func (c *jwtCacheFillerController) updateStatus( }) } - _ = conditionsutil.MergeConfigConditions( + _ = conditionsutil.MergeConditions( conditions, original.Generation, &updated.Status.Conditions, diff --git a/internal/controller/authenticator/webhookcachefiller/webhookcachefiller.go b/internal/controller/authenticator/webhookcachefiller/webhookcachefiller.go index 58f256c33..e0fd73af5 100644 --- a/internal/controller/authenticator/webhookcachefiller/webhookcachefiller.go +++ b/internal/controller/authenticator/webhookcachefiller/webhookcachefiller.go @@ -28,7 +28,6 @@ import ( conciergeclientset "go.pinniped.dev/generated/latest/client/concierge/clientset/versioned" authinformers "go.pinniped.dev/generated/latest/client/concierge/informers/externalversions/authentication/v1alpha1" pinnipedcontroller "go.pinniped.dev/internal/controller" - pinnipedauthenticator "go.pinniped.dev/internal/controller/authenticator" "go.pinniped.dev/internal/controller/authenticator/authncache" "go.pinniped.dev/internal/controller/conditionsutil" "go.pinniped.dev/internal/controllerlib" @@ -265,7 +264,7 @@ func (c *webhookCacheFillerController) validateConnection(certPool *x509.CertPoo } func (c *webhookCacheFillerController) validateTLSBundle(tlsSpec *authenticationv1alpha1.TLSSpec, conditions []*metav1.Condition) (*x509.CertPool, []byte, []*metav1.Condition, bool) { - rootCAs, pemBytes, err := pinnipedauthenticator.CABundle(tlsSpec) + rootCAs, pemBytes, err := pinnipedcontroller.BuildCertPoolAuth(tlsSpec) if err != nil { msg := fmt.Sprintf("%s: %s", "invalid TLS configuration", err.Error()) conditions = append(conditions, &metav1.Condition{ @@ -360,7 +359,7 @@ func (c *webhookCacheFillerController) updateStatus( }) } - _ = conditionsutil.MergeConfigConditions( + _ = conditionsutil.MergeConditions( conditions, original.Generation, &updated.Status.Conditions, diff --git a/internal/controller/conditionsutil/conditions_util.go b/internal/controller/conditionsutil/conditions_util.go index 7412db764..38004b907 100644 --- a/internal/controller/conditionsutil/conditions_util.go +++ b/internal/controller/conditionsutil/conditions_util.go @@ -12,29 +12,34 @@ import ( "go.pinniped.dev/internal/plog" ) -// MergeIDPConditions merges conditions into conditionsToUpdate. If returns true if it merged any error conditions. -func MergeIDPConditions(conditions []*metav1.Condition, observedGeneration int64, conditionsToUpdate *[]metav1.Condition, log plog.MinLogger) bool { - hadErrorCondition := false +// MergeConditions merges conditions into conditionsToUpdate. +// Note that LastTransitionTime refers to the time when the status changed, +// but ObservedGeneration should be the current generation for all conditions, since Pinniped should always check every condition. +// It returns true if any resulting condition has non-true status. +func MergeConditions( + conditions []*metav1.Condition, + observedGeneration int64, + conditionsToUpdate *[]metav1.Condition, + log plog.MinLogger, + lastTransitionTime metav1.Time, +) bool { for i := range conditions { cond := conditions[i].DeepCopy() - cond.LastTransitionTime = metav1.Now() + cond.LastTransitionTime = lastTransitionTime cond.ObservedGeneration = observedGeneration - if mergeIDPCondition(conditionsToUpdate, cond) { + if mergeCondition(conditionsToUpdate, cond) { log.Info("updated condition", "type", cond.Type, "status", cond.Status, "reason", cond.Reason, "message", cond.Message) } - if cond.Status == metav1.ConditionFalse { - hadErrorCondition = true - } } sort.SliceStable(*conditionsToUpdate, func(i, j int) bool { return (*conditionsToUpdate)[i].Type < (*conditionsToUpdate)[j].Type }) - return hadErrorCondition + return HadErrorCondition(conditions) } -// mergeIDPCondition merges a new metav1.Condition into a slice of existing conditions. It returns true +// mergeCondition merges a new metav1.Condition into a slice of existing conditions. It returns true // if the condition has meaningfully changed. -func mergeIDPCondition(existing *[]metav1.Condition, new *metav1.Condition) bool { +func mergeCondition(existing *[]metav1.Condition, new *metav1.Condition) bool { // Find any existing condition with a matching type. var old *metav1.Condition for i := range *existing { @@ -62,61 +67,7 @@ func mergeIDPCondition(existing *[]metav1.Condition, new *metav1.Condition) bool return true } - // Otherwise the entry is already up to date. - return false -} - -// MergeConfigConditions merges conditions into conditionsToUpdate. It returns true if it merged any error conditions. -func MergeConfigConditions(conditions []*metav1.Condition, observedGeneration int64, conditionsToUpdate *[]metav1.Condition, log plog.MinLogger, now metav1.Time) bool { - hadErrorCondition := false - for i := range conditions { - cond := conditions[i].DeepCopy() - cond.LastTransitionTime = now - cond.ObservedGeneration = observedGeneration - if mergeConfigCondition(conditionsToUpdate, cond) { - log.Info("updated condition", "type", cond.Type, "status", cond.Status, "reason", cond.Reason, "message", cond.Message) - } - if cond.Status == metav1.ConditionFalse { - hadErrorCondition = true - } - } - sort.SliceStable(*conditionsToUpdate, func(i, j int) bool { - return (*conditionsToUpdate)[i].Type < (*conditionsToUpdate)[j].Type - }) - return hadErrorCondition -} - -// mergeConfigCondition merges a new metav1.Condition into a slice of existing conditions. It returns true -// if the condition has meaningfully changed. -func mergeConfigCondition(existing *[]metav1.Condition, new *metav1.Condition) bool { - // Find any existing condition with a matching type. - var old *metav1.Condition - for i := range *existing { - if (*existing)[i].Type == new.Type { - old = &(*existing)[i] - continue - } - } - - // If there is no existing condition of this type, append this one and we're done. - if old == nil { - *existing = append(*existing, *new) - return true - } - - // Set the LastTransitionTime depending on whether the status has changed. - new = new.DeepCopy() - if old.Status == new.Status { - new.LastTransitionTime = old.LastTransitionTime - } - - // If anything has actually changed, update the entry and return true. - if !equality.Semantic.DeepEqual(old, new) { - *old = *new - return true - } - - // Otherwise the entry is already up to date. + // Otherwise the entry is already up-to-date. return false } diff --git a/internal/controller/conditionsutil/conditions_util_test.go b/internal/controller/conditionsutil/conditions_util_test.go new file mode 100644 index 000000000..ceb4aee53 --- /dev/null +++ b/internal/controller/conditionsutil/conditions_util_test.go @@ -0,0 +1,190 @@ +// Copyright 2024 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package conditionsutil + +import ( + "bytes" + "strings" + "testing" + "time" + + "github.com/stretchr/testify/require" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + "go.pinniped.dev/internal/plog" +) + +func TestMergeIDPConditions(t *testing.T) { + twoHoursAgo := metav1.Time{Time: time.Now().Add(-2 * time.Hour)} + oneHourAgo := metav1.Time{Time: time.Now().Add(-1 * time.Hour)} + testTime := metav1.Now() + + tests := []struct { + name string + newConditions []*metav1.Condition + conditionsToUpdate *[]metav1.Condition + observedGeneration int64 + wantResult bool + wantLogSnippets []string + wantConditions []metav1.Condition + }{ + { + name: "Adding a new condition with status=True returns false", + newConditions: []*metav1.Condition{ + { + Type: "NewType", + Status: metav1.ConditionTrue, + Reason: "new reason", + Message: "new message", + }, + }, + observedGeneration: int64(999), + conditionsToUpdate: &[]metav1.Condition{}, + wantLogSnippets: []string{ + `"message":"updated condition","type":"NewType","status":"True"`, + }, + wantConditions: []metav1.Condition{ + { + Type: "NewType", + Status: metav1.ConditionTrue, + ObservedGeneration: int64(999), + LastTransitionTime: testTime, + Reason: "new reason", + Message: "new message", + }, + }, + wantResult: false, + }, + { + name: "Updating a condition status from False to True returns true", + newConditions: []*metav1.Condition{ + { + Type: "UnchangedType", + Status: metav1.ConditionTrue, + Reason: "unchanged reason", + Message: "unchanged message", + }, + { + Type: "FalseToTrueType", + Status: metav1.ConditionFalse, + Reason: "new reason", + Message: "new message", + }, + { + Type: "NewType", + Status: metav1.ConditionTrue, + Reason: "new reason", + Message: "new message", + }, + }, + conditionsToUpdate: &[]metav1.Condition{ + { + Type: "UnchangedType", + Status: metav1.ConditionTrue, + ObservedGeneration: int64(10), + LastTransitionTime: twoHoursAgo, + Reason: "unchanged reason", + Message: "unchanged message", + }, + { + Type: "FalseToTrueType", + Status: metav1.ConditionTrue, + ObservedGeneration: int64(5), + LastTransitionTime: oneHourAgo, + Reason: "old reason", + Message: "old message", + }, + }, + observedGeneration: int64(100), + wantLogSnippets: []string{ + `"message":"updated condition","type":"UnchangedType","status":"True"`, + `"message":"updated condition","type":"NewType","status":"True"`, + `"message":"updated condition","type":"FalseToTrueType","status":"False"`, + }, + wantConditions: []metav1.Condition{ + { + Type: "FalseToTrueType", + Status: metav1.ConditionFalse, + ObservedGeneration: int64(100), + LastTransitionTime: testTime, + Reason: "new reason", + Message: "new message", + }, + { + Type: "NewType", + Status: metav1.ConditionTrue, + ObservedGeneration: int64(100), + LastTransitionTime: testTime, + Reason: "new reason", + Message: "new message", + }, + { + Type: "UnchangedType", + Status: metav1.ConditionTrue, + ObservedGeneration: int64(100), + LastTransitionTime: twoHoursAgo, + Reason: "unchanged reason", + Message: "unchanged message", + }, + }, + wantResult: true, + }, + { + name: "No logs when ObservedGeneration is unchanged", + newConditions: []*metav1.Condition{ + { + Type: "UnchangedType", + Status: metav1.ConditionFalse, + Reason: "unchanged reason", + Message: "unchanged message", + }, + }, + conditionsToUpdate: &[]metav1.Condition{ + { + Type: "UnchangedType", + Status: metav1.ConditionFalse, + ObservedGeneration: int64(10), + LastTransitionTime: twoHoursAgo, + Reason: "unchanged reason", + Message: "unchanged message", + }, + }, + observedGeneration: int64(10), + wantConditions: []metav1.Condition{ + { + Type: "UnchangedType", + Status: metav1.ConditionFalse, + ObservedGeneration: int64(10), + LastTransitionTime: twoHoursAgo, + Reason: "unchanged reason", + Message: "unchanged message", + }, + }, + wantResult: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var log bytes.Buffer + logger := plog.TestLogger(t, &log) + + result := MergeConditions( + tt.newConditions, + tt.observedGeneration, + tt.conditionsToUpdate, + logger, + testTime, + ) + + logString := log.String() + require.Equal(t, len(tt.wantLogSnippets), strings.Count(logString, "\n")) + for _, wantLog := range tt.wantLogSnippets { + require.Contains(t, logString, wantLog) + } + require.Equal(t, tt.wantResult, result) + require.Equal(t, tt.wantConditions, *tt.conditionsToUpdate) + }) + } +} diff --git a/internal/controller/supervisorconfig/activedirectoryupstreamwatcher/active_directory_upstream_watcher.go b/internal/controller/supervisorconfig/activedirectoryupstreamwatcher/active_directory_upstream_watcher.go index 0a29c06c8..efe64d055 100644 --- a/internal/controller/supervisorconfig/activedirectoryupstreamwatcher/active_directory_upstream_watcher.go +++ b/internal/controller/supervisorconfig/activedirectoryupstreamwatcher/active_directory_upstream_watcher.go @@ -344,7 +344,7 @@ func (c *activeDirectoryWatcherController) validateUpstream(ctx context.Context, UIDAttributeParsingOverrides: map[string]func(*ldap.Entry) (string, error){ "objectGUID": microsoftUUIDFromBinaryAttr("objectGUID"), }, - RefreshAttributeChecks: map[string]func(*ldap.Entry, upstreamprovider.RefreshAttributes) error{ + RefreshAttributeChecks: map[string]func(*ldap.Entry, upstreamprovider.LDAPRefreshAttributes) error{ pwdLastSetAttribute: attributeUnchangedSinceLogin(pwdLastSetAttribute), userAccountControlAttribute: validUserAccountControl, userAccountControlComputedAttribute: validComputedUserAccountControl, @@ -368,7 +368,7 @@ func (c *activeDirectoryWatcherController) updateStatus(ctx context.Context, ups log := plog.WithValues("namespace", upstream.Namespace, "name", upstream.Name) updated := upstream.DeepCopy() - hadErrorCondition := conditionsutil.MergeIDPConditions(conditions, upstream.Generation, &updated.Status.Conditions, log) + hadErrorCondition := conditionsutil.MergeConditions(conditions, upstream.Generation, &updated.Status.Conditions, log, metav1.Now()) updated.Status.Phase = idpv1alpha1.ActiveDirectoryPhaseReady if hadErrorCondition { @@ -445,7 +445,7 @@ func getDomainFromDistinguishedName(distinguishedName string) (string, error) { } //nolint:gochecknoglobals // this needs to be a global variable so that tests can check pointer equality -var validUserAccountControl = func(entry *ldap.Entry, _ upstreamprovider.RefreshAttributes) error { +var validUserAccountControl = func(entry *ldap.Entry, _ upstreamprovider.LDAPRefreshAttributes) error { userAccountControl, err := strconv.Atoi(entry.GetAttributeValue(userAccountControlAttribute)) if err != nil { return err @@ -459,7 +459,7 @@ var validUserAccountControl = func(entry *ldap.Entry, _ upstreamprovider.Refresh } //nolint:gochecknoglobals // this needs to be a global variable so that tests can check pointer equality -var validComputedUserAccountControl = func(entry *ldap.Entry, _ upstreamprovider.RefreshAttributes) error { +var validComputedUserAccountControl = func(entry *ldap.Entry, _ upstreamprovider.LDAPRefreshAttributes) error { userAccountControl, err := strconv.Atoi(entry.GetAttributeValue(userAccountControlComputedAttribute)) if err != nil { return err @@ -473,8 +473,8 @@ var validComputedUserAccountControl = func(entry *ldap.Entry, _ upstreamprovider } //nolint:gochecknoglobals // this needs to be a global variable so that tests can check pointer equality -var attributeUnchangedSinceLogin = func(attribute string) func(*ldap.Entry, upstreamprovider.RefreshAttributes) error { - return func(entry *ldap.Entry, storedAttributes upstreamprovider.RefreshAttributes) error { +var attributeUnchangedSinceLogin = func(attribute string) func(*ldap.Entry, upstreamprovider.LDAPRefreshAttributes) error { + return func(entry *ldap.Entry, storedAttributes upstreamprovider.LDAPRefreshAttributes) error { prevAttributeValue := storedAttributes.AdditionalAttributes[attribute] newValues := entry.GetRawAttributeValues(attribute) diff --git a/internal/controller/supervisorconfig/activedirectoryupstreamwatcher/active_directory_upstream_watcher_test.go b/internal/controller/supervisorconfig/activedirectoryupstreamwatcher/active_directory_upstream_watcher_test.go index c8bded274..80e47d7ca 100644 --- a/internal/controller/supervisorconfig/activedirectoryupstreamwatcher/active_directory_upstream_watcher_test.go +++ b/internal/controller/supervisorconfig/activedirectoryupstreamwatcher/active_directory_upstream_watcher_test.go @@ -228,7 +228,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) { GroupNameAttribute: testGroupSearchNameAttrName, }, UIDAttributeParsingOverrides: map[string]func(*ldap.Entry) (string, error){"objectGUID": microsoftUUIDFromBinaryAttr("objectGUID")}, - RefreshAttributeChecks: map[string]func(*ldap.Entry, upstreamprovider.RefreshAttributes) error{ + RefreshAttributeChecks: map[string]func(*ldap.Entry, upstreamprovider.LDAPRefreshAttributes) error{ "pwdLastSet": attributeUnchangedSinceLogin("pwdLastSet"), "userAccountControl": validUserAccountControl, "msDS-User-Account-Control-Computed": validComputedUserAccountControl, @@ -571,7 +571,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) { GroupNameAttribute: testGroupSearchNameAttrName, }, UIDAttributeParsingOverrides: map[string]func(*ldap.Entry) (string, error){"objectGUID": microsoftUUIDFromBinaryAttr("objectGUID")}, - RefreshAttributeChecks: map[string]func(*ldap.Entry, upstreamprovider.RefreshAttributes) error{ + RefreshAttributeChecks: map[string]func(*ldap.Entry, upstreamprovider.LDAPRefreshAttributes) error{ "pwdLastSet": attributeUnchangedSinceLogin("pwdLastSet"), "userAccountControl": validUserAccountControl, "msDS-User-Account-Control-Computed": validComputedUserAccountControl, @@ -641,7 +641,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) { GroupNameAttribute: "sAMAccountName", }, UIDAttributeParsingOverrides: map[string]func(*ldap.Entry) (string, error){"objectGUID": microsoftUUIDFromBinaryAttr("objectGUID")}, - RefreshAttributeChecks: map[string]func(*ldap.Entry, upstreamprovider.RefreshAttributes) error{ + RefreshAttributeChecks: map[string]func(*ldap.Entry, upstreamprovider.LDAPRefreshAttributes) error{ "pwdLastSet": attributeUnchangedSinceLogin("pwdLastSet"), "userAccountControl": validUserAccountControl, "msDS-User-Account-Control-Computed": validComputedUserAccountControl, @@ -714,7 +714,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) { GroupNameAttribute: testGroupSearchNameAttrName, }, UIDAttributeParsingOverrides: map[string]func(*ldap.Entry) (string, error){"objectGUID": microsoftUUIDFromBinaryAttr("objectGUID")}, - RefreshAttributeChecks: map[string]func(*ldap.Entry, upstreamprovider.RefreshAttributes) error{ + RefreshAttributeChecks: map[string]func(*ldap.Entry, upstreamprovider.LDAPRefreshAttributes) error{ "pwdLastSet": attributeUnchangedSinceLogin("pwdLastSet"), "userAccountControl": validUserAccountControl, "msDS-User-Account-Control-Computed": validComputedUserAccountControl, @@ -794,7 +794,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) { GroupNameAttribute: testGroupSearchNameAttrName, }, UIDAttributeParsingOverrides: map[string]func(*ldap.Entry) (string, error){"objectGUID": microsoftUUIDFromBinaryAttr("objectGUID")}, - RefreshAttributeChecks: map[string]func(*ldap.Entry, upstreamprovider.RefreshAttributes) error{ + RefreshAttributeChecks: map[string]func(*ldap.Entry, upstreamprovider.LDAPRefreshAttributes) error{ "pwdLastSet": attributeUnchangedSinceLogin("pwdLastSet"), "userAccountControl": validUserAccountControl, "msDS-User-Account-Control-Computed": validComputedUserAccountControl, @@ -858,7 +858,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) { GroupNameAttribute: testGroupSearchNameAttrName, }, UIDAttributeParsingOverrides: map[string]func(*ldap.Entry) (string, error){"objectGUID": microsoftUUIDFromBinaryAttr("objectGUID")}, - RefreshAttributeChecks: map[string]func(*ldap.Entry, upstreamprovider.RefreshAttributes) error{ + RefreshAttributeChecks: map[string]func(*ldap.Entry, upstreamprovider.LDAPRefreshAttributes) error{ "pwdLastSet": attributeUnchangedSinceLogin("pwdLastSet"), "userAccountControl": validUserAccountControl, "msDS-User-Account-Control-Computed": validComputedUserAccountControl, @@ -1009,7 +1009,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) { GroupNameAttribute: testGroupSearchNameAttrName, }, UIDAttributeParsingOverrides: map[string]func(*ldap.Entry) (string, error){"objectGUID": microsoftUUIDFromBinaryAttr("objectGUID")}, - RefreshAttributeChecks: map[string]func(*ldap.Entry, upstreamprovider.RefreshAttributes) error{ + RefreshAttributeChecks: map[string]func(*ldap.Entry, upstreamprovider.LDAPRefreshAttributes) error{ "pwdLastSet": attributeUnchangedSinceLogin("pwdLastSet"), "userAccountControl": validUserAccountControl, "msDS-User-Account-Control-Computed": validComputedUserAccountControl, @@ -1159,7 +1159,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) { GroupNameAttribute: testGroupSearchNameAttrName, }, UIDAttributeParsingOverrides: map[string]func(*ldap.Entry) (string, error){"objectGUID": microsoftUUIDFromBinaryAttr("objectGUID")}, - RefreshAttributeChecks: map[string]func(*ldap.Entry, upstreamprovider.RefreshAttributes) error{ + RefreshAttributeChecks: map[string]func(*ldap.Entry, upstreamprovider.LDAPRefreshAttributes) error{ "pwdLastSet": attributeUnchangedSinceLogin("pwdLastSet"), "userAccountControl": validUserAccountControl, "msDS-User-Account-Control-Computed": validComputedUserAccountControl, @@ -1231,7 +1231,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) { GroupNameAttribute: testGroupSearchNameAttrName, }, UIDAttributeParsingOverrides: map[string]func(*ldap.Entry) (string, error){"objectGUID": microsoftUUIDFromBinaryAttr("objectGUID")}, - RefreshAttributeChecks: map[string]func(*ldap.Entry, upstreamprovider.RefreshAttributes) error{ + RefreshAttributeChecks: map[string]func(*ldap.Entry, upstreamprovider.LDAPRefreshAttributes) error{ "pwdLastSet": attributeUnchangedSinceLogin("pwdLastSet"), "userAccountControl": validUserAccountControl, "msDS-User-Account-Control-Computed": validComputedUserAccountControl, @@ -1498,7 +1498,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) { }, UIDAttributeParsingOverrides: map[string]func(*ldap.Entry) (string, error){"objectGUID": microsoftUUIDFromBinaryAttr("objectGUID")}, GroupAttributeParsingOverrides: map[string]func(*ldap.Entry) (string, error){"sAMAccountName": groupSAMAccountNameWithDomainSuffix}, - RefreshAttributeChecks: map[string]func(*ldap.Entry, upstreamprovider.RefreshAttributes) error{ + RefreshAttributeChecks: map[string]func(*ldap.Entry, upstreamprovider.LDAPRefreshAttributes) error{ "pwdLastSet": attributeUnchangedSinceLogin("pwdLastSet"), "userAccountControl": validUserAccountControl, "msDS-User-Account-Control-Computed": validComputedUserAccountControl, @@ -1558,7 +1558,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) { GroupNameAttribute: testGroupSearchNameAttrName, }, UIDAttributeParsingOverrides: map[string]func(*ldap.Entry) (string, error){"objectGUID": microsoftUUIDFromBinaryAttr("objectGUID")}, - RefreshAttributeChecks: map[string]func(*ldap.Entry, upstreamprovider.RefreshAttributes) error{ + RefreshAttributeChecks: map[string]func(*ldap.Entry, upstreamprovider.LDAPRefreshAttributes) error{ "pwdLastSet": attributeUnchangedSinceLogin("pwdLastSet"), "userAccountControl": validUserAccountControl, "msDS-User-Account-Control-Computed": validComputedUserAccountControl, @@ -1622,7 +1622,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) { GroupNameAttribute: testGroupSearchNameAttrName, }, UIDAttributeParsingOverrides: map[string]func(*ldap.Entry) (string, error){"objectGUID": microsoftUUIDFromBinaryAttr("objectGUID")}, - RefreshAttributeChecks: map[string]func(*ldap.Entry, upstreamprovider.RefreshAttributes) error{ + RefreshAttributeChecks: map[string]func(*ldap.Entry, upstreamprovider.LDAPRefreshAttributes) error{ "pwdLastSet": attributeUnchangedSinceLogin("pwdLastSet"), "userAccountControl": validUserAccountControl, "msDS-User-Account-Control-Computed": validComputedUserAccountControl, @@ -1686,7 +1686,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) { GroupNameAttribute: testGroupSearchNameAttrName, }, UIDAttributeParsingOverrides: map[string]func(*ldap.Entry) (string, error){"objectGUID": microsoftUUIDFromBinaryAttr("objectGUID")}, - RefreshAttributeChecks: map[string]func(*ldap.Entry, upstreamprovider.RefreshAttributes) error{ + RefreshAttributeChecks: map[string]func(*ldap.Entry, upstreamprovider.LDAPRefreshAttributes) error{ "pwdLastSet": attributeUnchangedSinceLogin("pwdLastSet"), "userAccountControl": validUserAccountControl, "msDS-User-Account-Control-Computed": validComputedUserAccountControl, @@ -1898,7 +1898,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) { GroupNameAttribute: testGroupSearchNameAttrName, }, UIDAttributeParsingOverrides: map[string]func(*ldap.Entry) (string, error){"objectGUID": microsoftUUIDFromBinaryAttr("objectGUID")}, - RefreshAttributeChecks: map[string]func(*ldap.Entry, upstreamprovider.RefreshAttributes) error{ + RefreshAttributeChecks: map[string]func(*ldap.Entry, upstreamprovider.LDAPRefreshAttributes) error{ "pwdLastSet": attributeUnchangedSinceLogin("pwdLastSet"), "userAccountControl": validUserAccountControl, "msDS-User-Account-Control-Computed": validComputedUserAccountControl, @@ -1961,7 +1961,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) { SkipGroupRefresh: true, }, UIDAttributeParsingOverrides: map[string]func(*ldap.Entry) (string, error){"objectGUID": microsoftUUIDFromBinaryAttr("objectGUID")}, - RefreshAttributeChecks: map[string]func(*ldap.Entry, upstreamprovider.RefreshAttributes) error{ + RefreshAttributeChecks: map[string]func(*ldap.Entry, upstreamprovider.LDAPRefreshAttributes) error{ "pwdLastSet": attributeUnchangedSinceLogin("pwdLastSet"), "userAccountControl": validUserAccountControl, "msDS-User-Account-Control-Computed": validComputedUserAccountControl, @@ -2102,8 +2102,8 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) { expectedRefreshAttributeChecks := copyOfExpectedValueForResultingCache.RefreshAttributeChecks actualRefreshAttributeChecks := actualConfig.RefreshAttributeChecks - copyOfExpectedValueForResultingCache.RefreshAttributeChecks = map[string]func(*ldap.Entry, upstreamprovider.RefreshAttributes) error{} - actualConfig.RefreshAttributeChecks = map[string]func(*ldap.Entry, upstreamprovider.RefreshAttributes) error{} + copyOfExpectedValueForResultingCache.RefreshAttributeChecks = map[string]func(*ldap.Entry, upstreamprovider.LDAPRefreshAttributes) error{} + actualConfig.RefreshAttributeChecks = map[string]func(*ldap.Entry, upstreamprovider.LDAPRefreshAttributes) error{} require.Equal(t, len(expectedRefreshAttributeChecks), len(actualRefreshAttributeChecks)) for k, v := range expectedRefreshAttributeChecks { require.NotNil(t, actualRefreshAttributeChecks[k]) @@ -2352,7 +2352,7 @@ func TestValidUserAccountControl(t *testing.T) { for _, test := range tests { tt := test t.Run(tt.name, func(t *testing.T) { - err := validUserAccountControl(tt.entry, upstreamprovider.RefreshAttributes{}) + err := validUserAccountControl(tt.entry, upstreamprovider.LDAPRefreshAttributes{}) if tt.wantErr != "" { require.Error(t, err) @@ -2413,7 +2413,7 @@ func TestValidComputedUserAccountControl(t *testing.T) { for _, test := range tests { tt := test t.Run(tt.name, func(t *testing.T) { - err := validComputedUserAccountControl(tt.entry, upstreamprovider.RefreshAttributes{}) + err := validComputedUserAccountControl(tt.entry, upstreamprovider.LDAPRefreshAttributes{}) if tt.wantErr != "" { require.Error(t, err) @@ -2488,7 +2488,7 @@ func TestAttributeUnchangedSinceLogin(t *testing.T) { tt := test t.Run(tt.name, func(t *testing.T) { initialValRawEncoded := base64.RawURLEncoding.EncodeToString([]byte(initialVal)) - err := attributeUnchangedSinceLogin(attributeName)(tt.entry, upstreamprovider.RefreshAttributes{AdditionalAttributes: map[string]string{attributeName: initialValRawEncoded}}) + err := attributeUnchangedSinceLogin(attributeName)(tt.entry, upstreamprovider.LDAPRefreshAttributes{AdditionalAttributes: map[string]string{attributeName: initialValRawEncoded}}) if tt.wantErr != "" { require.Error(t, err) require.Equal(t, tt.wantErr, err.Error()) diff --git a/internal/controller/supervisorconfig/federation_domain_watcher.go b/internal/controller/supervisorconfig/federation_domain_watcher.go index 99eb66a02..b15173819 100644 --- a/internal/controller/supervisorconfig/federation_domain_watcher.go +++ b/internal/controller/supervisorconfig/federation_domain_watcher.go @@ -67,6 +67,7 @@ const ( kindLDAPIdentityProvider = "LDAPIdentityProvider" kindOIDCIdentityProvider = "OIDCIdentityProvider" kindActiveDirectoryIdentityProvider = "ActiveDirectoryIdentityProvider" + kindGitHubIdentityProvider = "GitHubIdentityProvider" celTransformerMaxExpressionRuntime = 5 * time.Second ) @@ -88,6 +89,7 @@ type federationDomainWatcherController struct { oidcIdentityProviderInformer idpinformers.OIDCIdentityProviderInformer ldapIdentityProviderInformer idpinformers.LDAPIdentityProviderInformer activeDirectoryIdentityProviderInformer idpinformers.ActiveDirectoryIdentityProviderInformer + githubIdentityProviderInformer idpinformers.GitHubIdentityProviderInformer celTransformer *celtransformer.CELTransformer allowedKinds sets.Set[string] @@ -104,9 +106,10 @@ func NewFederationDomainWatcherController( oidcIdentityProviderInformer idpinformers.OIDCIdentityProviderInformer, ldapIdentityProviderInformer idpinformers.LDAPIdentityProviderInformer, activeDirectoryIdentityProviderInformer idpinformers.ActiveDirectoryIdentityProviderInformer, + githubProviderInformer idpinformers.GitHubIdentityProviderInformer, withInformer pinnipedcontroller.WithInformerOptionFunc, ) controllerlib.Controller { - allowedKinds := sets.New(kindActiveDirectoryIdentityProvider, kindLDAPIdentityProvider, kindOIDCIdentityProvider) + allowedKinds := sets.New(kindActiveDirectoryIdentityProvider, kindLDAPIdentityProvider, kindOIDCIdentityProvider, kindGitHubIdentityProvider) return controllerlib.New( controllerlib.Config{ Name: controllerName, @@ -119,6 +122,7 @@ func NewFederationDomainWatcherController( oidcIdentityProviderInformer: oidcIdentityProviderInformer, ldapIdentityProviderInformer: ldapIdentityProviderInformer, activeDirectoryIdentityProviderInformer: activeDirectoryIdentityProviderInformer, + githubIdentityProviderInformer: githubProviderInformer, allowedKinds: allowedKinds, }, }, @@ -148,6 +152,13 @@ func NewFederationDomainWatcherController( pinnipedcontroller.MatchAnythingIgnoringUpdatesFilter(pinnipedcontroller.SingletonQueue()), controllerlib.InformerOption{}, ), + withInformer( + githubProviderInformer, + // Since this controller only cares about IDP metadata names and UIDs (immutable fields), + // we only need to trigger Sync on creates and deletes. + pinnipedcontroller.MatchAnythingIgnoringUpdatesFilter(pinnipedcontroller.SingletonQueue()), + controllerlib.InformerOption{}, + ), ) } @@ -264,9 +275,12 @@ func (c *federationDomainWatcherController) makeLegacyFederationDomainIssuer( if err != nil { return nil, nil, err } - + githubIdentityProviders, err := c.githubIdentityProviderInformer.Lister().List(labels.Everything()) + if err != nil { + return nil, nil, err + } // Check if that there is exactly one IDP defined in the Supervisor namespace of any IDP CRD type. - idpCRsCount := len(oidcIdentityProviders) + len(ldapIdentityProviders) + len(activeDirectoryIdentityProviders) + idpCRsCount := len(oidcIdentityProviders) + len(ldapIdentityProviders) + len(activeDirectoryIdentityProviders) + len(githubIdentityProviders) switch { case idpCRsCount == 1: @@ -286,6 +300,10 @@ func (c *federationDomainWatcherController) makeLegacyFederationDomainIssuer( defaultFederationDomainIdentityProvider.DisplayName = activeDirectoryIdentityProviders[0].Name defaultFederationDomainIdentityProvider.UID = activeDirectoryIdentityProviders[0].UID foundIDPName = activeDirectoryIdentityProviders[0].Name + case len(githubIdentityProviders) == 1: + defaultFederationDomainIdentityProvider.DisplayName = githubIdentityProviders[0].Name + defaultFederationDomainIdentityProvider.UID = githubIdentityProviders[0].UID + foundIDPName = githubIdentityProviders[0].Name } // Backwards compatibility mode always uses an empty identity transformation pipeline since no // transformations are defined on the FederationDomain. @@ -446,6 +464,8 @@ func (c *federationDomainWatcherController) findIDPsUIDByObjectRef(objectRef cor foundIDP, err = c.activeDirectoryIdentityProviderInformer.Lister().ActiveDirectoryIdentityProviders(namespace).Get(objectRef.Name) case kindOIDCIdentityProvider: foundIDP, err = c.oidcIdentityProviderInformer.Lister().OIDCIdentityProviders(namespace).Get(objectRef.Name) + case kindGitHubIdentityProvider: + foundIDP, err = c.githubIdentityProviderInformer.Lister().GitHubIdentityProviders(namespace).Get(objectRef.Name) default: // This shouldn't happen because this helper function is not called when the kind is invalid. return "", false, fmt.Errorf("unexpected kind: %s", objectRef.Kind) @@ -813,7 +833,7 @@ func (c *federationDomainWatcherController) updateStatus( }) } - _ = conditionsutil.MergeConfigConditions(conditions, + _ = conditionsutil.MergeConditions(conditions, federationDomain.Generation, &updated.Status.Conditions, plog.New().WithName(controllerName), metav1.NewTime(c.clock.Now())) if equality.Semantic.DeepEqual(federationDomain, updated) { diff --git a/internal/controller/supervisorconfig/federation_domain_watcher_test.go b/internal/controller/supervisorconfig/federation_domain_watcher_test.go index d51c4f61b..b606f9c87 100644 --- a/internal/controller/supervisorconfig/federation_domain_watcher_test.go +++ b/internal/controller/supervisorconfig/federation_domain_watcher_test.go @@ -42,6 +42,7 @@ func TestFederationDomainWatcherControllerInformerFilters(t *testing.T) { oidcIdentityProviderInformer := supervisorinformers.NewSharedInformerFactoryWithOptions(nil, 0).IDP().V1alpha1().OIDCIdentityProviders() ldapIdentityProviderInformer := supervisorinformers.NewSharedInformerFactoryWithOptions(nil, 0).IDP().V1alpha1().LDAPIdentityProviders() adIdentityProviderInformer := supervisorinformers.NewSharedInformerFactoryWithOptions(nil, 0).IDP().V1alpha1().ActiveDirectoryIdentityProviders() + githubIdentityProviderInformer := supervisorinformers.NewSharedInformerFactoryWithOptions(nil, 0).IDP().V1alpha1().GitHubIdentityProviders() tests := []struct { name string @@ -82,6 +83,13 @@ func TestFederationDomainWatcherControllerInformerFilters(t *testing.T) { wantAdd: true, wantUpdate: false, wantDelete: true, + }, { + name: "any GitHubIdentityProvider adds or deletes, but updates are ignored", + obj: &idpv1alpha1.GitHubIdentityProvider{}, + informer: githubIdentityProviderInformer, + wantAdd: true, + wantUpdate: false, + wantDelete: true, }, } for _, test := range tests { @@ -99,6 +107,7 @@ func TestFederationDomainWatcherControllerInformerFilters(t *testing.T) { oidcIdentityProviderInformer, ldapIdentityProviderInformer, adIdentityProviderInformer, + githubIdentityProviderInformer, withInformer.WithInformer, // make it possible to observe the behavior of the Filters ) @@ -162,6 +171,14 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { }, } + gitHubIdentityProvider := &idpv1alpha1.GitHubIdentityProvider{ + ObjectMeta: metav1.ObjectMeta{ + Name: "some-github-idp", + Namespace: namespace, + UID: "some-github-idp", + }, + } + federationDomain1 := &supervisorconfigv1alpha1.FederationDomain{ ObjectMeta: metav1.ObjectMeta{Name: "config1", Namespace: namespace, Generation: 123}, Spec: supervisorconfigv1alpha1.FederationDomainSpec{Issuer: "https://issuer1.com"}, @@ -486,7 +503,7 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { LastTransitionTime: time, Reason: "KindUnrecognized", Message: fmt.Sprintf(`some kinds specified by .spec.identityProviders[].objectRef.kind are `+ - `not recognized (should be one of "ActiveDirectoryIdentityProvider", "LDAPIdentityProvider", "OIDCIdentityProvider"): %s`, badKinds), + `not recognized (should be one of "ActiveDirectoryIdentityProvider", "GitHubIdentityProvider", "LDAPIdentityProvider", "OIDCIdentityProvider"): %s`, badKinds), } } @@ -603,6 +620,30 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { ), }, }, + { + name: "legacy config: when no identity provider is specified on federation domains, but exactly one GitHub identity " + + "provider resource exists on cluster, the controller will set a default IDP on each federation domain " + + "matching the only identity provider found", + inputObjects: []runtime.Object{ + federationDomain1, + federationDomain2, + gitHubIdentityProvider, + }, + wantFDIssuers: []*federationdomainproviders.FederationDomainIssuer{ + federationDomainIssuerWithDefaultIDP(t, federationDomain1.Spec.Issuer, gitHubIdentityProvider.ObjectMeta), + federationDomainIssuerWithDefaultIDP(t, federationDomain2.Spec.Issuer, gitHubIdentityProvider.ObjectMeta), + }, + wantStatusUpdates: []*configv1alpha1.FederationDomain{ + expectedFederationDomainStatusUpdate(federationDomain1, + configv1alpha1.FederationDomainPhaseReady, + allHappyConditionsLegacyConfigurationSuccess(federationDomain1.Spec.Issuer, gitHubIdentityProvider.Name, frozenMetav1Now, 123), + ), + expectedFederationDomainStatusUpdate(federationDomain2, + configv1alpha1.FederationDomainPhaseReady, + allHappyConditionsLegacyConfigurationSuccess(federationDomain2.Spec.Issuer, gitHubIdentityProvider.Name, frozenMetav1Now, 123), + ), + }, + }, { name: "when there are two valid FederationDomains, but one is already up to date, the sync loop only updates " + "the out-of-date FederationDomain", @@ -947,6 +988,7 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { oidcIdentityProvider, ldapIdentityProvider, adIdentityProvider, + gitHubIdentityProvider, }, wantFDIssuers: []*federationdomainproviders.FederationDomainIssuer{}, wantStatusUpdates: []*supervisorconfigv1alpha1.FederationDomain{ @@ -955,7 +997,7 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { conditionstestutil.Replace( allHappyConditionsLegacyConfigurationSuccess(federationDomain1.Spec.Issuer, "", frozenMetav1Now, 123), []metav1.Condition{ - sadIdentityProvidersFoundConditionIdentityProviderNotSpecified(3, frozenMetav1Now, 123), + sadIdentityProvidersFoundConditionIdentityProviderNotSpecified(4, frozenMetav1Now, 123), sadReadyCondition(frozenMetav1Now, 123), }), ), @@ -993,6 +1035,14 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { Name: "cant-find-me-still-name", }, }, + { + DisplayName: "cant-find-me-again", + ObjectRef: corev1.TypedLocalObjectReference{ + APIGroup: ptr.To(apiGroupSupervisor), + Kind: "GitHubIdentityProvider", + Name: "cant-find-me-again-name", + }, + }, }, }, }, @@ -1012,7 +1062,9 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { cannot find resource specified by .spec.identityProviders[1].objectRef (with name "cant-find-me-either-name") - cannot find resource specified by .spec.identityProviders[2].objectRef (with name "cant-find-me-still-name")`, + cannot find resource specified by .spec.identityProviders[2].objectRef (with name "cant-find-me-still-name") + + cannot find resource specified by .spec.identityProviders[3].objectRef (with name "cant-find-me-again-name")`, ), frozenMetav1Now, 123), sadReadyCondition(frozenMetav1Now, 123), }), @@ -1025,6 +1077,7 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { oidcIdentityProvider, ldapIdentityProvider, adIdentityProvider, + gitHubIdentityProvider, &supervisorconfigv1alpha1.FederationDomain{ ObjectMeta: metav1.ObjectMeta{Name: "config1", Namespace: namespace, Generation: 123}, Spec: supervisorconfigv1alpha1.FederationDomainSpec{ @@ -1054,6 +1107,14 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { Name: adIdentityProvider.Name, }, }, + { + DisplayName: "can-find-me-four", + ObjectRef: corev1.TypedLocalObjectReference{ + APIGroup: ptr.To(apiGroupSupervisor), + Kind: "GitHubIdentityProvider", + Name: gitHubIdentityProvider.Name, + }, + }, }, }, }, @@ -1076,6 +1137,11 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { UID: adIdentityProvider.UID, Transforms: idtransform.NewTransformationPipeline(), }, + { + DisplayName: "can-find-me-four", + UID: gitHubIdentityProvider.UID, + Transforms: idtransform.NewTransformationPipeline(), + }, }), }, wantStatusUpdates: []*supervisorconfigv1alpha1.FederationDomain{ @@ -1094,6 +1160,7 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { oidcIdentityProvider, ldapIdentityProvider, adIdentityProvider, + gitHubIdentityProvider, &supervisorconfigv1alpha1.FederationDomain{ ObjectMeta: metav1.ObjectMeta{Name: "config1", Namespace: namespace, Generation: 123}, Spec: supervisorconfigv1alpha1.FederationDomainSpec{ @@ -1147,6 +1214,14 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { Name: adIdentityProvider.Name, }, }, + { + DisplayName: "duplicate2", + ObjectRef: corev1.TypedLocalObjectReference{ + APIGroup: ptr.To(apiGroupSupervisor), + Kind: "GitHubIdentityProvider", + Name: gitHubIdentityProvider.Name, + }, + }, }, }, }, @@ -1173,6 +1248,7 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { oidcIdentityProvider, ldapIdentityProvider, adIdentityProvider, + gitHubIdentityProvider, &supervisorconfigv1alpha1.FederationDomain{ ObjectMeta: metav1.ObjectMeta{Name: "config1", Namespace: namespace, Generation: 123}, Spec: supervisorconfigv1alpha1.FederationDomainSpec{ @@ -1210,6 +1286,14 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { Name: adIdentityProvider.Name, }, }, + { + DisplayName: "name5", + ObjectRef: corev1.TypedLocalObjectReference{ + APIGroup: ptr.To(apiGroupSupervisor), // correct + Kind: "GitHubIdentityProvider", + Name: gitHubIdentityProvider.Name, + }, + }, }, }, }, @@ -1243,6 +1327,7 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { oidcIdentityProvider, ldapIdentityProvider, adIdentityProvider, + gitHubIdentityProvider, &supervisorconfigv1alpha1.FederationDomain{ ObjectMeta: metav1.ObjectMeta{Name: "config1", Namespace: namespace, Generation: 123}, Spec: supervisorconfigv1alpha1.FederationDomainSpec{ @@ -2022,6 +2107,7 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) { pinnipedInformers.IDP().V1alpha1().OIDCIdentityProviders(), pinnipedInformers.IDP().V1alpha1().LDAPIdentityProviders(), pinnipedInformers.IDP().V1alpha1().ActiveDirectoryIdentityProviders(), + pinnipedInformers.IDP().V1alpha1().GitHubIdentityProviders(), controllerlib.WithInformer, ) diff --git a/internal/controller/supervisorconfig/githubupstreamwatcher/github_upstream_watcher.go b/internal/controller/supervisorconfig/githubupstreamwatcher/github_upstream_watcher.go new file mode 100644 index 000000000..b1de18d0c --- /dev/null +++ b/internal/controller/supervisorconfig/githubupstreamwatcher/github_upstream_watcher.go @@ -0,0 +1,511 @@ +// Copyright 2020-2024 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +// Package githubupstreamwatcher implements a controller which watches GitHubIdentityProviders. +package githubupstreamwatcher + +import ( + "context" + "crypto/tls" + "crypto/x509" + "errors" + "fmt" + "net" + "net/http" + "slices" + "strings" + + "golang.org/x/oauth2" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/equality" + k8sapierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/labels" + errorsutil "k8s.io/apimachinery/pkg/util/errors" + k8sutilerrors "k8s.io/apimachinery/pkg/util/errors" + corev1informers "k8s.io/client-go/informers/core/v1" + "k8s.io/utils/clock" + + "go.pinniped.dev/generated/latest/apis/supervisor/idp/v1alpha1" + supervisorclientset "go.pinniped.dev/generated/latest/client/supervisor/clientset/versioned" + idpinformers "go.pinniped.dev/generated/latest/client/supervisor/informers/externalversions/idp/v1alpha1" + pinnipedcontroller "go.pinniped.dev/internal/controller" + "go.pinniped.dev/internal/controller/conditionsutil" + "go.pinniped.dev/internal/controller/supervisorconfig/upstreamwatchers" + "go.pinniped.dev/internal/controllerlib" + "go.pinniped.dev/internal/crypto/ptls" + "go.pinniped.dev/internal/endpointaddr" + "go.pinniped.dev/internal/federationdomain/upstreamprovider" + "go.pinniped.dev/internal/net/phttp" + "go.pinniped.dev/internal/plog" + "go.pinniped.dev/internal/setutil" + "go.pinniped.dev/internal/upstreamgithub" +) + +const ( + controllerName = "github-upstream-observer" + + // Constants related to the client credentials Secret. + gitHubClientSecretType corev1.SecretType = "secrets.pinniped.dev/github-client" + clientIDDataKey, clientSecretDataKey string = "clientID", "clientSecret" + + countExpectedConditions = 6 + + HostValid string = "HostValid" + TLSConfigurationValid string = "TLSConfigurationValid" + OrganizationsPolicyValid string = "OrganizationsPolicyValid" + ClientCredentialsSecretValid string = "ClientCredentialsSecretValid" //nolint:gosec // this is not a credential + GitHubConnectionValid string = "GitHubConnectionValid" + ClaimsValid string = "ClaimsValid" + + defaultHost = "github.com" + defaultApiBaseURL = "https://api.github.com" +) + +// UpstreamGitHubIdentityProviderICache is a thread safe cache that holds a list of validated upstream GitHub IDP configurations. +type UpstreamGitHubIdentityProviderICache interface { + SetGitHubIdentityProviders([]upstreamprovider.UpstreamGithubIdentityProviderI) +} + +type gitHubWatcherController struct { + namespace string + cache UpstreamGitHubIdentityProviderICache + log plog.Logger + client supervisorclientset.Interface + gitHubIdentityProviderInformer idpinformers.GitHubIdentityProviderInformer + secretInformer corev1informers.SecretInformer + clock clock.Clock + dialFunc func(network, addr string, config *tls.Config) (*tls.Conn, error) +} + +// New instantiates a new controllerlib.Controller which will populate the provided UpstreamGitHubIdentityProviderICache. +func New( + namespace string, + idpCache UpstreamGitHubIdentityProviderICache, + client supervisorclientset.Interface, + gitHubIdentityProviderInformer idpinformers.GitHubIdentityProviderInformer, + secretInformer corev1informers.SecretInformer, + log plog.Logger, + withInformer pinnipedcontroller.WithInformerOptionFunc, + clock clock.Clock, + dialFunc func(network, addr string, config *tls.Config) (*tls.Conn, error), +) controllerlib.Controller { + c := gitHubWatcherController{ + namespace: namespace, + cache: idpCache, + client: client, + log: log.WithName(controllerName), + gitHubIdentityProviderInformer: gitHubIdentityProviderInformer, + secretInformer: secretInformer, + clock: clock, + dialFunc: dialFunc, + } + + return controllerlib.New( + controllerlib.Config{Name: controllerName, Syncer: &c}, + withInformer( + gitHubIdentityProviderInformer, + pinnipedcontroller.SimpleFilter(func(obj metav1.Object) bool { + gitHubIDP, ok := obj.(*v1alpha1.GitHubIdentityProvider) + return ok && gitHubIDP.Namespace == namespace + }, pinnipedcontroller.SingletonQueue()), + controllerlib.InformerOption{}, + ), + withInformer( + secretInformer, + pinnipedcontroller.MatchAnySecretOfTypeFilter(gitHubClientSecretType, pinnipedcontroller.SingletonQueue(), namespace), + controllerlib.InformerOption{}, + ), + ) +} + +// Sync implements controllerlib.Syncer. +func (c *gitHubWatcherController) Sync(ctx controllerlib.Context) error { + actualUpstreams, err := c.gitHubIdentityProviderInformer.Lister().List(labels.Everything()) + if err != nil { // untested + return fmt.Errorf("failed to list GitHubIdentityProviders: %w", err) + } + + // Sort them by name just so that the logs output is consistent + slices.SortStableFunc(actualUpstreams, func(a, b *v1alpha1.GitHubIdentityProvider) int { + return strings.Compare(a.Name, b.Name) + }) + + var applicationErrors []error + validatedUpstreams := make([]upstreamprovider.UpstreamGithubIdentityProviderI, 0, len(actualUpstreams)) + for _, upstream := range actualUpstreams { + validatedUpstream, applicationErr := c.validateUpstreamAndUpdateConditions(ctx, upstream) + if applicationErr != nil { + applicationErrors = append(applicationErrors, applicationErr) + } else if validatedUpstream != nil { + validatedUpstreams = append(validatedUpstreams, validatedUpstream) + } + // Else: + // If both validatedUpstream and applicationErr are nil, this must be because the upstream had configuration errors. + // This controller should take no action until the user has reconfigured the upstream. + } + c.cache.SetGitHubIdentityProviders(validatedUpstreams) + + // If we have recoverable application errors, let's do a requeue and capture all the applicationErrors too + if len(applicationErrors) > 0 { + applicationErrors = append([]error{controllerlib.ErrSyntheticRequeue}, applicationErrors...) + } + + return errorsutil.NewAggregate(applicationErrors) +} + +func (c *gitHubWatcherController) validateClientSecret(secretName string) (*metav1.Condition, string, string, error) { + secret, unableToRetrieveSecretErr := c.secretInformer.Lister().Secrets(c.namespace).Get(secretName) + + // This error requires user interaction, so ignore it. + if k8sapierrors.IsNotFound(unableToRetrieveSecretErr) { + unableToRetrieveSecretErr = nil + } + + buildFalseCondition := func(prefix string) (*metav1.Condition, string, string, error) { + return &metav1.Condition{ + Type: ClientCredentialsSecretValid, + Status: metav1.ConditionFalse, + Reason: upstreamwatchers.ReasonNotFound, + Message: fmt.Sprintf("%s: secret from spec.client.SecretName (%q) must be found in namespace %q with type %q and keys %q and %q", + prefix, + secretName, + c.namespace, + gitHubClientSecretType, + clientIDDataKey, + clientSecretDataKey), + }, "", "", unableToRetrieveSecretErr + } + + if unableToRetrieveSecretErr != nil || secret == nil { + return buildFalseCondition(fmt.Sprintf("secret %q not found", secretName)) + } + + if secret.Type != gitHubClientSecretType { + return buildFalseCondition(fmt.Sprintf("wrong secret type %q", secret.Type)) + } + + clientID := string(secret.Data[clientIDDataKey]) + if len(clientID) < 1 { + return buildFalseCondition(fmt.Sprintf("missing key %q", clientIDDataKey)) + } + + clientSecret := string(secret.Data[clientSecretDataKey]) + if len(clientSecret) < 1 { + return buildFalseCondition(fmt.Sprintf("missing key %q", clientSecretDataKey)) + } + + if len(secret.Data) != 2 { + return buildFalseCondition("extra keys found") + } + + return &metav1.Condition{ + Type: ClientCredentialsSecretValid, + Status: metav1.ConditionTrue, + Reason: upstreamwatchers.ReasonSuccess, + Message: fmt.Sprintf("clientID and clientSecret have been read from spec.client.SecretName (%q)", secretName), + }, clientID, clientSecret, nil +} + +func validateOrganizationsPolicy(organizationsSpec *v1alpha1.GitHubOrganizationsSpec) *metav1.Condition { + var policy v1alpha1.GitHubAllowedAuthOrganizationsPolicy + if organizationsSpec.Policy != nil { + policy = *organizationsSpec.Policy + } + + // Should not happen due to CRD defaulting, enum validation, and CEL validation (for recent versions of K8s only!) + // That is why the message here is very minimal + if (policy == v1alpha1.GitHubAllowedAuthOrganizationsPolicyAllGitHubUsers && len(organizationsSpec.Allowed) == 0) || + (policy == v1alpha1.GitHubAllowedAuthOrganizationsPolicyOnlyUsersFromAllowedOrganizations && len(organizationsSpec.Allowed) > 0) { + return &metav1.Condition{ + Type: OrganizationsPolicyValid, + Status: metav1.ConditionTrue, + Reason: upstreamwatchers.ReasonSuccess, + Message: fmt.Sprintf("spec.allowAuthentication.organizations.policy (%q) is valid", policy), + } + } + + if len(organizationsSpec.Allowed) > 0 { + return &metav1.Condition{ + Type: OrganizationsPolicyValid, + Status: metav1.ConditionFalse, + Reason: "Invalid", + Message: "spec.allowAuthentication.organizations.policy must be 'OnlyUsersFromAllowedOrganizations' when spec.allowAuthentication.organizations.allowed has organizations listed", + } + } + + return &metav1.Condition{ + Type: OrganizationsPolicyValid, + Status: metav1.ConditionFalse, + Reason: "Invalid", + Message: "spec.allowAuthentication.organizations.policy must be 'AllGitHubUsers' when spec.allowAuthentication.organizations.allowed is empty", + } +} + +func (c *gitHubWatcherController) validateUpstreamAndUpdateConditions(ctx controllerlib.Context, upstream *v1alpha1.GitHubIdentityProvider) ( + *upstreamgithub.Provider, // If validated, returns the config + error, // This error will only refer to programmatic errors such as inability to perform a Dial or dereference a pointer, not configuration errors +) { + conditions := make([]*metav1.Condition, 0) + applicationErrors := make([]error, 0) + + clientSecretCondition, clientID, clientSecret, clientSecretErr := c.validateClientSecret(upstream.Spec.Client.SecretName) + conditions = append(conditions, clientSecretCondition) + if clientSecretErr != nil { // untested + applicationErrors = append(applicationErrors, clientSecretErr) + } + + // Should there be some sort of catch-all condition to capture this? + // This does not actually prevent a GitHub IDP from being added to the cache. + // CRD defaulting and validation should eliminate the possibility of an error here. + userAndGroupCondition, groupNameAttribute, usernameAttribute := validateUserAndGroupAttributes(upstream) + conditions = append(conditions, userAndGroupCondition) + + organizationPolicyCondition := validateOrganizationsPolicy(&upstream.Spec.AllowAuthentication.Organizations) + conditions = append(conditions, organizationPolicyCondition) + + hostCondition, hostPort := validateHost(upstream.Spec.GitHubAPI) + conditions = append(conditions, hostCondition) + + tlsConfigCondition, certPool := c.validateTLSConfiguration(upstream.Spec.GitHubAPI.TLS) + conditions = append(conditions, tlsConfigCondition) + + githubConnectionCondition, hostURL, httpClient, githubConnectionErr := c.validateGitHubConnection( + hostPort, + certPool, + hostCondition.Status == metav1.ConditionTrue && tlsConfigCondition.Status == metav1.ConditionTrue, + ) + if githubConnectionErr != nil { + applicationErrors = append(applicationErrors, githubConnectionErr) + } + conditions = append(conditions, githubConnectionCondition) + + // The critical pattern to maintain is that every run of the sync loop will populate the exact number of the exact + // same set of conditions. Conditions depending on other conditions should get Status: metav1.ConditionUnknown, or + // Status: metav1.ConditionFalse, never be omitted. + if len(conditions) != countExpectedConditions { // untested since all code paths return the same number of conditions + applicationErrors = append(applicationErrors, fmt.Errorf("expected %d conditions but found %d conditions", countExpectedConditions, len(conditions))) + return nil, k8sutilerrors.NewAggregate(applicationErrors) + } + hadErrorCondition, updateStatusErr := c.updateStatus(ctx.Context, upstream, conditions) + if updateStatusErr != nil { + applicationErrors = append(applicationErrors, updateStatusErr) + } + // Any error condition means we will not add the IDP to the cache, so just return nil here + if hadErrorCondition { + return nil, k8sutilerrors.NewAggregate(applicationErrors) + } + + provider := upstreamgithub.New( + upstreamgithub.ProviderConfig{ + Name: upstream.Name, + ResourceUID: upstream.UID, + APIBaseURL: apiBaseUrl(*upstream.Spec.GitHubAPI.Host, hostURL), + GroupNameAttribute: groupNameAttribute, + UsernameAttribute: usernameAttribute, + OAuth2Config: &oauth2.Config{ + ClientID: clientID, + ClientSecret: clientSecret, + // See https://docs.github.com/en/apps/oauth-apps/building-oauth-apps/authorizing-oauth-apps + Endpoint: oauth2.Endpoint{ + AuthURL: fmt.Sprintf("%s/login/oauth/authorize", hostURL), + DeviceAuthURL: "", // we do not use device code flow + TokenURL: fmt.Sprintf("%s/login/oauth/access_token", hostURL), + AuthStyle: oauth2.AuthStyleInParams, + }, + RedirectURL: "", // this will be different for each FederationDomain, so we do not set it here + Scopes: []string{"read:user", "read:org"}, + }, + AllowedOrganizations: setutil.NewCaseInsensitiveSet(upstream.Spec.AllowAuthentication.Organizations.Allowed...), + HttpClient: httpClient, + }, + ) + return provider, k8sutilerrors.NewAggregate(applicationErrors) +} + +func apiBaseUrl(upstreamSpecHost string, hostURL string) string { + if upstreamSpecHost != defaultHost { + return fmt.Sprintf("%s/api/v3", hostURL) + } + return defaultApiBaseURL +} + +func validateHost(gitHubAPIConfig v1alpha1.GitHubAPIConfig) (*metav1.Condition, *endpointaddr.HostPort) { + buildInvalidHost := func(host, reason string) *metav1.Condition { + return &metav1.Condition{ + Type: HostValid, + Status: metav1.ConditionFalse, + Reason: "InvalidHost", + Message: fmt.Sprintf("spec.githubAPI.host (%q) is not valid: %s", host, reason), + } + } + + // Should not happen due to CRD defaulting + if gitHubAPIConfig.Host == nil || len(*gitHubAPIConfig.Host) < 1 { + return buildInvalidHost("", "must not be empty"), nil + } + + host := *gitHubAPIConfig.Host + hostPort, addressParseErr := endpointaddr.Parse(host, 443) + if addressParseErr != nil { + // addressParseErr is not recoverable. It requires user interaction, so do not return the error. + return buildInvalidHost(host, addressParseErr.Error()), nil + } + + return &metav1.Condition{ + Type: HostValid, + Status: metav1.ConditionTrue, + Reason: upstreamwatchers.ReasonSuccess, + Message: fmt.Sprintf("spec.githubAPI.host (%q) is valid", host), + }, &hostPort +} + +func (c *gitHubWatcherController) validateTLSConfiguration(tlsSpec *v1alpha1.TLSSpec) (*metav1.Condition, *x509.CertPool) { + certPool, _, buildCertPoolErr := pinnipedcontroller.BuildCertPoolIDP(tlsSpec) + if buildCertPoolErr != nil { + // buildCertPoolErr is not recoverable with a resync. + // It requires user interaction, so do not return the error. + return &metav1.Condition{ + Type: TLSConfigurationValid, + Status: metav1.ConditionFalse, + Reason: "InvalidTLSConfig", + Message: fmt.Sprintf("spec.githubAPI.tls.certificateAuthorityData is not valid: %s", buildCertPoolErr), + }, nil + } + + return &metav1.Condition{ + Type: TLSConfigurationValid, + Status: metav1.ConditionTrue, + Reason: upstreamwatchers.ReasonSuccess, + Message: "spec.githubAPI.tls.certificateAuthorityData is valid", + }, certPool +} + +func (c *gitHubWatcherController) validateGitHubConnection( + hostPort *endpointaddr.HostPort, + certPool *x509.CertPool, + validSoFar bool, +) (*metav1.Condition, string, *http.Client, error) { + if !validSoFar { + return &metav1.Condition{ + Type: GitHubConnectionValid, + Status: metav1.ConditionUnknown, + Reason: "UnableToValidate", + Message: "unable to validate; see other conditions for details", + }, "", nil, nil + } + + conn, tlsDialErr := c.dialFunc("tcp", hostPort.Endpoint(), ptls.Default(certPool)) + if tlsDialErr != nil { + return &metav1.Condition{ + Type: GitHubConnectionValid, + Status: metav1.ConditionFalse, + Reason: "UnableToDialServer", + Message: fmt.Sprintf("cannot dial server spec.githubAPI.host (%q): %s", hostPort.Endpoint(), buildDialErrorMessage(tlsDialErr)), + }, "", nil, tlsDialErr + } + + return &metav1.Condition{ + Type: GitHubConnectionValid, + Status: metav1.ConditionTrue, + Reason: upstreamwatchers.ReasonSuccess, + Message: fmt.Sprintf("spec.githubAPI.host (%q) is reachable and TLS verification succeeds", hostPort.Endpoint()), + }, fmt.Sprintf("https://%s", hostPort.Endpoint()), phttp.Default(certPool), conn.Close() +} + +// buildDialErrorMessage standardizes DNS error messages that appear differently on different platforms, so that tests and log grepping is uniform. +func buildDialErrorMessage(tlsDialErr error) string { + reason := tlsDialErr.Error() + + var opError *net.OpError + var dnsError *net.DNSError + if errors.As(tlsDialErr, &opError) && errors.As(tlsDialErr, &dnsError) { + dnsError.Server = "" + opError.Err = dnsError + return opError.Error() + } + + return reason +} + +func validateUserAndGroupAttributes(upstream *v1alpha1.GitHubIdentityProvider) (*metav1.Condition, v1alpha1.GitHubGroupNameAttribute, v1alpha1.GitHubUsernameAttribute) { + buildInvalidCondition := func(message string) *metav1.Condition { + return &metav1.Condition{ + Type: ClaimsValid, + Status: metav1.ConditionFalse, + Reason: "Invalid", + Message: message, + } + } + + var usernameAttribute v1alpha1.GitHubUsernameAttribute + if upstream.Spec.Claims.Username == nil { + return buildInvalidCondition("spec.claims.username is required"), "", "" + } else { + usernameAttribute = *upstream.Spec.Claims.Username + } + + var groupNameAttribute v1alpha1.GitHubGroupNameAttribute + if upstream.Spec.Claims.Groups == nil { + return buildInvalidCondition("spec.claims.groups is required"), "", "" + } else { + groupNameAttribute = *upstream.Spec.Claims.Groups + } + + switch usernameAttribute { + case v1alpha1.GitHubUsernameLoginAndID: + case v1alpha1.GitHubUsernameLogin: + case v1alpha1.GitHubUsernameID: + default: + // Should not happen due to CRD enum validation + return buildInvalidCondition(fmt.Sprintf("spec.claims.username (%q) is not valid", usernameAttribute)), "", "" + } + + switch groupNameAttribute { + case v1alpha1.GitHubUseTeamNameForGroupName: + case v1alpha1.GitHubUseTeamSlugForGroupName: + default: + // Should not happen due to CRD enum validation + return buildInvalidCondition(fmt.Sprintf("spec.claims.groups (%q) is not valid", groupNameAttribute)), "", "" + } + + return &metav1.Condition{ + Type: ClaimsValid, + Status: metav1.ConditionTrue, + Reason: upstreamwatchers.ReasonSuccess, + Message: "spec.claims are valid", + }, groupNameAttribute, usernameAttribute +} + +func (c *gitHubWatcherController) updateStatus( + ctx context.Context, + upstream *v1alpha1.GitHubIdentityProvider, + conditions []*metav1.Condition) (bool, error) { + log := c.log.WithValues("namespace", upstream.Namespace, "name", upstream.Name) + updated := upstream.DeepCopy() + + hadErrorCondition := conditionsutil.MergeConditions( + conditions, + upstream.Generation, + &updated.Status.Conditions, + log, + metav1.NewTime(c.clock.Now()), + ) + + updated.Status.Phase = v1alpha1.GitHubPhaseReady + if hadErrorCondition { + updated.Status.Phase = v1alpha1.GitHubPhaseError + } + + if equality.Semantic.DeepEqual(upstream, updated) { + return hadErrorCondition, nil + } + + log.Info("updating GitHubIdentityProvider status", "phase", updated.Status.Phase) + + _, updateStatusError := c.client. + IDPV1alpha1(). + GitHubIdentityProviders(upstream.Namespace). + UpdateStatus(ctx, updated, metav1.UpdateOptions{}) + return hadErrorCondition, updateStatusError +} diff --git a/internal/controller/supervisorconfig/githubupstreamwatcher/github_upstream_watcher_test.go b/internal/controller/supervisorconfig/githubupstreamwatcher/github_upstream_watcher_test.go new file mode 100644 index 000000000..0da18dff8 --- /dev/null +++ b/internal/controller/supervisorconfig/githubupstreamwatcher/github_upstream_watcher_test.go @@ -0,0 +1,2423 @@ +// Copyright 2020-2024 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package githubupstreamwatcher + +import ( + "bytes" + "context" + "crypto/tls" + "encoding/base64" + "fmt" + "net" + "net/http" + "strings" + "testing" + "time" + + "github.com/stretchr/testify/require" + "golang.org/x/oauth2" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/types" + utilnet "k8s.io/apimachinery/pkg/util/net" + k8sinformers "k8s.io/client-go/informers" + kubernetesfake "k8s.io/client-go/kubernetes/fake" + coretesting "k8s.io/client-go/testing" + "k8s.io/utils/clock" + clocktesting "k8s.io/utils/clock/testing" + "k8s.io/utils/ptr" + + "go.pinniped.dev/generated/latest/apis/supervisor/idp/v1alpha1" + supervisorfake "go.pinniped.dev/generated/latest/client/supervisor/clientset/versioned/fake" + pinnipedinformers "go.pinniped.dev/generated/latest/client/supervisor/informers/externalversions" + "go.pinniped.dev/internal/certauthority" + pinnipedcontroller "go.pinniped.dev/internal/controller" + "go.pinniped.dev/internal/controller/supervisorconfig/upstreamwatchers" + "go.pinniped.dev/internal/controllerlib" + "go.pinniped.dev/internal/federationdomain/dynamicupstreamprovider" + "go.pinniped.dev/internal/federationdomain/upstreamprovider" + "go.pinniped.dev/internal/net/phttp" + "go.pinniped.dev/internal/plog" + "go.pinniped.dev/internal/setutil" + "go.pinniped.dev/internal/testutil" + "go.pinniped.dev/internal/testutil/tlsserver" + "go.pinniped.dev/internal/upstreamgithub" +) + +var ( + githubIDPGVR = schema.GroupVersionResource{ + Group: v1alpha1.SchemeGroupVersion.Group, + Version: v1alpha1.SchemeGroupVersion.Version, + Resource: "githubidentityproviders", + } + + githubIDPKind = v1alpha1.SchemeGroupVersion.WithKind("GitHubIdentityProvider") +) + +func TestController(t *testing.T) { + require.Equal(t, 6, countExpectedConditions) + + goodServer, goodServerCA := tlsserver.TestServerIPv4(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + }), tlsserver.RecordTLSHello) + goodServerDomain, _ := strings.CutPrefix(goodServer.URL, "https://") + goodServerCAB64 := base64.StdEncoding.EncodeToString(goodServerCA) + + goodServerIPv6, goodServerIPv6CA := tlsserver.TestServerIPv6(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + }), tlsserver.RecordTLSHello) + goodServerIPv6Domain, _ := strings.CutPrefix(goodServerIPv6.URL, "https://") + goodServerIPv6CAB64 := base64.StdEncoding.EncodeToString(goodServerIPv6CA) + + caForUnknownServer, err := certauthority.New("Some Unknown CA", time.Hour) + require.NoError(t, err) + unknownServerCABytes, _, err := caForUnknownServer.IssueServerCertPEM( + []string{"some-dns-name", "some-other-dns-name"}, + []net.IP{net.ParseIP("10.2.3.4")}, + time.Hour, + ) + require.NoError(t, err) + + wantObservedGeneration := int64(1234) + namespace := "some-namespace" + + wantFrozenTime := time.Date(1122, time.September, 33, 4, 55, 56, 778899, time.Local) + frozenClockForLastTransitionTime := clocktesting.NewFakeClock(wantFrozenTime) + wantLastTransitionTime := metav1.Time{Time: wantFrozenTime} + + goodSecret := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "some-secret-name", + Namespace: namespace, + }, + Type: "secrets.pinniped.dev/github-client", + Data: map[string][]byte{ + "clientID": []byte("some-client-id"), + "clientSecret": []byte("some-client-secret"), + }, + } + + validMinimalIDP := &v1alpha1.GitHubIdentityProvider{ + ObjectMeta: metav1.ObjectMeta{ + Name: "minimal-idp-name", + Namespace: namespace, + UID: types.UID("minimal-uid"), + Generation: wantObservedGeneration, + }, + Spec: v1alpha1.GitHubIdentityProviderSpec{ + GitHubAPI: v1alpha1.GitHubAPIConfig{ + Host: ptr.To(goodServerDomain), + TLS: &v1alpha1.TLSSpec{ + CertificateAuthorityData: goodServerCAB64, + }, + }, + Client: v1alpha1.GitHubClientSpec{ + SecretName: goodSecret.Name, + }, + // These claims are optional when using the actual Kubernetes CRD. + // However, they are required here because CRD defaulting/validation does not occur during testing. + Claims: v1alpha1.GitHubClaims{ + Username: ptr.To(v1alpha1.GitHubUsernameLogin), + Groups: ptr.To(v1alpha1.GitHubUseTeamSlugForGroupName), + }, + AllowAuthentication: v1alpha1.GitHubAllowAuthenticationSpec{ + Organizations: v1alpha1.GitHubOrganizationsSpec{ + Policy: ptr.To(v1alpha1.GitHubAllowedAuthOrganizationsPolicyAllGitHubUsers), + }, + }, + }, + } + + validFilledOutIDP := &v1alpha1.GitHubIdentityProvider{ + ObjectMeta: metav1.ObjectMeta{ + Name: "some-idp-name", + Namespace: namespace, + UID: types.UID("some-resource-uid"), + Generation: wantObservedGeneration, + }, + Spec: v1alpha1.GitHubIdentityProviderSpec{ + GitHubAPI: v1alpha1.GitHubAPIConfig{ + Host: ptr.To(goodServerDomain), + TLS: &v1alpha1.TLSSpec{ + CertificateAuthorityData: goodServerCAB64, + }, + }, + Claims: v1alpha1.GitHubClaims{ + Username: ptr.To(v1alpha1.GitHubUsernameID), + Groups: ptr.To(v1alpha1.GitHubUseTeamNameForGroupName), + }, + AllowAuthentication: v1alpha1.GitHubAllowAuthenticationSpec{ + Organizations: v1alpha1.GitHubOrganizationsSpec{ + Policy: ptr.To(v1alpha1.GitHubAllowedAuthOrganizationsPolicyOnlyUsersFromAllowedOrganizations), + Allowed: []string{"organization1", "org2"}, + }, + }, + Client: v1alpha1.GitHubClientSpec{ + SecretName: goodSecret.Name, + }, + }, + } + + buildHostValidTrue := func(t *testing.T, host string) metav1.Condition { + t.Helper() + + return metav1.Condition{ + Type: HostValid, + Status: metav1.ConditionTrue, + ObservedGeneration: wantObservedGeneration, + LastTransitionTime: wantLastTransitionTime, + Reason: upstreamwatchers.ReasonSuccess, + Message: fmt.Sprintf("spec.githubAPI.host (%q) is valid", host), + } + } + + buildHostValidFalse := func(t *testing.T, host, message string) metav1.Condition { + t.Helper() + + return metav1.Condition{ + Type: HostValid, + Status: metav1.ConditionFalse, + ObservedGeneration: wantObservedGeneration, + LastTransitionTime: wantLastTransitionTime, + Reason: "InvalidHost", + Message: fmt.Sprintf(`spec.githubAPI.host (%q) is not valid: %s`, host, message), + } + } + + buildTLSConfigurationValidTrue := func(t *testing.T) metav1.Condition { + t.Helper() + + return metav1.Condition{ + Type: TLSConfigurationValid, + Status: metav1.ConditionTrue, + ObservedGeneration: wantObservedGeneration, + LastTransitionTime: wantLastTransitionTime, + Reason: upstreamwatchers.ReasonSuccess, + Message: "spec.githubAPI.tls.certificateAuthorityData is valid", + } + } + + buildTLSConfigurationValidFalse := func(t *testing.T, message string) metav1.Condition { + t.Helper() + + return metav1.Condition{ + Type: TLSConfigurationValid, + Status: metav1.ConditionFalse, + ObservedGeneration: wantObservedGeneration, + LastTransitionTime: wantLastTransitionTime, + Reason: "InvalidTLSConfig", + Message: message, + } + } + + buildOrganizationsPolicyValidTrue := func(t *testing.T, policy v1alpha1.GitHubAllowedAuthOrganizationsPolicy) metav1.Condition { + t.Helper() + + return metav1.Condition{ + Type: OrganizationsPolicyValid, + Status: metav1.ConditionTrue, + ObservedGeneration: wantObservedGeneration, + LastTransitionTime: wantLastTransitionTime, + Reason: upstreamwatchers.ReasonSuccess, + Message: fmt.Sprintf("spec.allowAuthentication.organizations.policy (%q) is valid", policy), + } + } + + buildOrganizationsPolicyValidFalse := func(t *testing.T, message string) metav1.Condition { + t.Helper() + + return metav1.Condition{ + Type: OrganizationsPolicyValid, + Status: metav1.ConditionFalse, + ObservedGeneration: wantObservedGeneration, + LastTransitionTime: wantLastTransitionTime, + Reason: "Invalid", + Message: message, + } + } + + buildClientCredentialsSecretValidTrue := func(t *testing.T, secretName string) metav1.Condition { + t.Helper() + + return metav1.Condition{ + Type: ClientCredentialsSecretValid, + Status: metav1.ConditionTrue, + ObservedGeneration: wantObservedGeneration, + LastTransitionTime: wantLastTransitionTime, + Reason: upstreamwatchers.ReasonSuccess, + Message: fmt.Sprintf("clientID and clientSecret have been read from spec.client.SecretName (%q)", secretName), + } + } + + buildClientCredentialsSecretValidFalse := func(t *testing.T, prefix, secretName, namespace, reason string) metav1.Condition { + t.Helper() + + return metav1.Condition{ + Type: ClientCredentialsSecretValid, + Status: metav1.ConditionFalse, + ObservedGeneration: wantObservedGeneration, + LastTransitionTime: wantLastTransitionTime, + Reason: reason, + Message: fmt.Sprintf( + `%s: secret from spec.client.SecretName (%q) must be found in namespace %q with type "secrets.pinniped.dev/github-client" and keys "clientID" and "clientSecret"`, + prefix, + secretName, + namespace, + ), + } + } + + buildClaimsValidatedTrue := func(t *testing.T) metav1.Condition { + t.Helper() + + return metav1.Condition{ + Type: ClaimsValid, + Status: metav1.ConditionTrue, + ObservedGeneration: wantObservedGeneration, + LastTransitionTime: wantLastTransitionTime, + Reason: upstreamwatchers.ReasonSuccess, + Message: "spec.claims are valid", + } + } + + buildClaimsValidatedFalse := func(t *testing.T, message string) metav1.Condition { + t.Helper() + + return metav1.Condition{ + Type: ClaimsValid, + Status: metav1.ConditionFalse, + ObservedGeneration: wantObservedGeneration, + LastTransitionTime: wantLastTransitionTime, + Reason: "Invalid", + Message: message, + } + } + + buildGitHubConnectionValidTrue := func(t *testing.T, host string) metav1.Condition { + t.Helper() + + return metav1.Condition{ + Type: GitHubConnectionValid, + Status: metav1.ConditionTrue, + ObservedGeneration: wantObservedGeneration, + LastTransitionTime: wantLastTransitionTime, + Reason: upstreamwatchers.ReasonSuccess, + Message: fmt.Sprintf("spec.githubAPI.host (%q) is reachable and TLS verification succeeds", host), + } + } + + buildGitHubConnectionValidFalse := func(t *testing.T, message string) metav1.Condition { + t.Helper() + + return metav1.Condition{ + Type: GitHubConnectionValid, + Status: metav1.ConditionFalse, + ObservedGeneration: wantObservedGeneration, + LastTransitionTime: wantLastTransitionTime, + Reason: "UnableToDialServer", + Message: message, + } + } + + buildGitHubConnectionValidUnknown := func(t *testing.T) metav1.Condition { + t.Helper() + + return metav1.Condition{ + Type: GitHubConnectionValid, + Status: metav1.ConditionUnknown, + ObservedGeneration: wantObservedGeneration, + LastTransitionTime: wantLastTransitionTime, + Reason: "UnableToValidate", + Message: "unable to validate; see other conditions for details", + } + } + + buildLogForUpdatingClaimsValidTrue := func(name string) string { + return fmt.Sprintf(`{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","logger":"github-upstream-observer","caller":"conditionsutil/conditions_util.go:$conditionsutil.MergeConditions","message":"updated condition","namespace":"some-namespace","name":"%s","type":"ClaimsValid","status":"True","reason":"Success","message":"spec.claims are valid"}`, name) + } + + buildLogForUpdatingClaimsValidFalse := func(name, message string) string { + return fmt.Sprintf(`{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","logger":"github-upstream-observer","caller":"conditionsutil/conditions_util.go:$conditionsutil.MergeConditions","message":"updated condition","namespace":"some-namespace","name":"%s","type":"ClaimsValid","status":"False","reason":"Invalid","message":"%s"}`, name, message) + } + + buildLogForUpdatingClientCredentialsSecretValid := func(name, status, reason, message string) string { + return fmt.Sprintf(`{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","logger":"github-upstream-observer","caller":"conditionsutil/conditions_util.go:$conditionsutil.MergeConditions","message":"updated condition","namespace":"some-namespace","name":"%s","type":"ClientCredentialsSecretValid","status":"%s","reason":"%s","message":"%s"}`, name, status, reason, message) + } + + buildLogForUpdatingOrganizationPolicyValid := func(name, status, reason, message string) string { + return fmt.Sprintf(`{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","logger":"github-upstream-observer","caller":"conditionsutil/conditions_util.go:$conditionsutil.MergeConditions","message":"updated condition","namespace":"some-namespace","name":"%s","type":"OrganizationsPolicyValid","status":"%s","reason":"%s","message":"%s"}`, name, status, reason, message) + } + + buildLogForUpdatingHostValid := func(name, status, reason, messageFmt, host string) string { + return fmt.Sprintf(`{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","logger":"github-upstream-observer","caller":"conditionsutil/conditions_util.go:$conditionsutil.MergeConditions","message":"updated condition","namespace":"some-namespace","name":"%s","type":"HostValid","status":"%s","reason":"%s","message":"%s"}`, name, status, reason, fmt.Sprintf(messageFmt, host)) + } + + buildLogForUpdatingTLSConfigurationValid := func(name, status, reason, message string) string { + return fmt.Sprintf(`{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","logger":"github-upstream-observer","caller":"conditionsutil/conditions_util.go:$conditionsutil.MergeConditions","message":"updated condition","namespace":"some-namespace","name":"%s","type":"TLSConfigurationValid","status":"%s","reason":"%s","message":"%s"}`, name, status, reason, message) + } + + buildLogForUpdatingGitHubConnectionValid := func(name, status, reason, messageFmt, host string) string { + return fmt.Sprintf(`{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","logger":"github-upstream-observer","caller":"conditionsutil/conditions_util.go:$conditionsutil.MergeConditions","message":"updated condition","namespace":"some-namespace","name":"%s","type":"GitHubConnectionValid","status":"%s","reason":"%s","message":"%s"}`, name, status, reason, fmt.Sprintf(messageFmt, host)) + } + + buildLogForUpdatingGitHubConnectionValidUnknown := func(name string) string { + return fmt.Sprintf(`{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","logger":"github-upstream-observer","caller":"conditionsutil/conditions_util.go:$conditionsutil.MergeConditions","message":"updated condition","namespace":"some-namespace","name":"%s","type":"GitHubConnectionValid","status":"Unknown","reason":"UnableToValidate","message":"unable to validate; see other conditions for details"}`, name) + } + + buildLogForUpdatingPhase := func(name, phase string) string { + return fmt.Sprintf(`{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","logger":"github-upstream-observer","caller":"githubupstreamwatcher/github_upstream_watcher.go:$githubupstreamwatcher.(*gitHubWatcherController).updateStatus","message":"updating GitHubIdentityProvider status","namespace":"some-namespace","name":"%s","phase":"%s"}`, name, phase) + } + + tests := []struct { + name string + githubIdentityProviders []runtime.Object + secrets []runtime.Object + mockDialer func(network, addr string, config *tls.Config) (*tls.Conn, error) + wantErr string + wantLogs []string + wantResultingCache []*upstreamgithub.ProviderConfig + wantResultingUpstreams []v1alpha1.GitHubIdentityProvider + }{ + { + name: "no GitHubIdentityProviders", + wantResultingCache: []*upstreamgithub.ProviderConfig{}, + wantResultingUpstreams: []v1alpha1.GitHubIdentityProvider{}, + wantLogs: []string{}, + }, + { + name: "happy path with all fields", + secrets: []runtime.Object{goodSecret}, + githubIdentityProviders: []runtime.Object{ + validFilledOutIDP, + }, + wantResultingCache: []*upstreamgithub.ProviderConfig{ + { + Name: "some-idp-name", + ResourceUID: "some-resource-uid", + APIBaseURL: fmt.Sprintf("https://%s/api/v3", *validFilledOutIDP.Spec.GitHubAPI.Host), + UsernameAttribute: "id", + GroupNameAttribute: "name", + OAuth2Config: &oauth2.Config{ + ClientID: "some-client-id", + ClientSecret: "some-client-secret", + Endpoint: oauth2.Endpoint{ + AuthURL: fmt.Sprintf("https://%s/login/oauth/authorize", *validFilledOutIDP.Spec.GitHubAPI.Host), + DeviceAuthURL: "", // not used + TokenURL: fmt.Sprintf("https://%s/login/oauth/access_token", *validFilledOutIDP.Spec.GitHubAPI.Host), + AuthStyle: oauth2.AuthStyleInParams, + }, + RedirectURL: "", // not used + Scopes: []string{"read:user", "read:org"}, + }, + AllowedOrganizations: setutil.NewCaseInsensitiveSet("organization1", "org2"), + HttpClient: nil, // let the test runner populate this for us + }, + }, + wantResultingUpstreams: []v1alpha1.GitHubIdentityProvider{ + { + ObjectMeta: validFilledOutIDP.ObjectMeta, + Spec: validFilledOutIDP.Spec, + Status: v1alpha1.GitHubIdentityProviderStatus{ + Phase: v1alpha1.GitHubPhaseReady, + Conditions: []metav1.Condition{ + buildClaimsValidatedTrue(t), + buildClientCredentialsSecretValidTrue(t, validFilledOutIDP.Spec.Client.SecretName), + buildGitHubConnectionValidTrue(t, *validFilledOutIDP.Spec.GitHubAPI.Host), + buildHostValidTrue(t, *validFilledOutIDP.Spec.GitHubAPI.Host), + buildOrganizationsPolicyValidTrue(t, *validFilledOutIDP.Spec.AllowAuthentication.Organizations.Policy), + buildTLSConfigurationValidTrue(t), + }, + }, + }, + }, + wantLogs: []string{ + buildLogForUpdatingClientCredentialsSecretValid("some-idp-name", "True", "Success", fmt.Sprintf(`clientID and clientSecret have been read from spec.client.SecretName (\"%s\")`, validFilledOutIDP.Spec.Client.SecretName)), + buildLogForUpdatingClaimsValidTrue("some-idp-name"), + buildLogForUpdatingOrganizationPolicyValid("some-idp-name", "True", "Success", fmt.Sprintf(`spec.allowAuthentication.organizations.policy (\"%s\") is valid`, string(*validFilledOutIDP.Spec.AllowAuthentication.Organizations.Policy))), + buildLogForUpdatingHostValid("some-idp-name", "True", "Success", `spec.githubAPI.host (\"%s\") is valid`, *validFilledOutIDP.Spec.GitHubAPI.Host), + buildLogForUpdatingTLSConfigurationValid("some-idp-name", "True", "Success", "spec.githubAPI.tls.certificateAuthorityData is valid"), + buildLogForUpdatingGitHubConnectionValid("some-idp-name", "True", "Success", `spec.githubAPI.host (\"%s\") is reachable and TLS verification succeeds`, *validFilledOutIDP.Spec.GitHubAPI.Host), + buildLogForUpdatingPhase("some-idp-name", "Ready"), + }, + }, + { + name: "happy path with minimal fields", + secrets: []runtime.Object{goodSecret}, + githubIdentityProviders: []runtime.Object{ + validMinimalIDP, + }, + wantResultingCache: []*upstreamgithub.ProviderConfig{ + { + Name: "minimal-idp-name", + ResourceUID: "minimal-uid", + APIBaseURL: fmt.Sprintf("https://%s/api/v3", *validFilledOutIDP.Spec.GitHubAPI.Host), + UsernameAttribute: "login", + GroupNameAttribute: "slug", + OAuth2Config: &oauth2.Config{ + ClientID: "some-client-id", + ClientSecret: "some-client-secret", + Endpoint: oauth2.Endpoint{ + AuthURL: fmt.Sprintf("https://%s/login/oauth/authorize", *validFilledOutIDP.Spec.GitHubAPI.Host), + DeviceAuthURL: "", // not used + TokenURL: fmt.Sprintf("https://%s/login/oauth/access_token", *validFilledOutIDP.Spec.GitHubAPI.Host), + AuthStyle: oauth2.AuthStyleInParams, + }, + RedirectURL: "", // not used + Scopes: []string{"read:user", "read:org"}, + }, + AllowedOrganizations: setutil.NewCaseInsensitiveSet(), + HttpClient: nil, // let the test runner populate this for us + }, + }, + wantResultingUpstreams: []v1alpha1.GitHubIdentityProvider{ + { + ObjectMeta: validMinimalIDP.ObjectMeta, + Spec: validMinimalIDP.Spec, + Status: v1alpha1.GitHubIdentityProviderStatus{ + Phase: v1alpha1.GitHubPhaseReady, + Conditions: []metav1.Condition{ + buildClaimsValidatedTrue(t), + buildClientCredentialsSecretValidTrue(t, validMinimalIDP.Spec.Client.SecretName), + buildGitHubConnectionValidTrue(t, *validMinimalIDP.Spec.GitHubAPI.Host), + buildHostValidTrue(t, *validMinimalIDP.Spec.GitHubAPI.Host), + buildOrganizationsPolicyValidTrue(t, *validMinimalIDP.Spec.AllowAuthentication.Organizations.Policy), + buildTLSConfigurationValidTrue(t), + }, + }, + }, + }, + wantLogs: []string{ + buildLogForUpdatingClientCredentialsSecretValid("minimal-idp-name", "True", "Success", fmt.Sprintf(`clientID and clientSecret have been read from spec.client.SecretName (\"%s\")`, validMinimalIDP.Spec.Client.SecretName)), + buildLogForUpdatingClaimsValidTrue("minimal-idp-name"), + buildLogForUpdatingOrganizationPolicyValid("minimal-idp-name", "True", "Success", fmt.Sprintf(`spec.allowAuthentication.organizations.policy (\"%s\") is valid`, string(*validMinimalIDP.Spec.AllowAuthentication.Organizations.Policy))), + buildLogForUpdatingHostValid("minimal-idp-name", "True", "Success", `spec.githubAPI.host (\"%s\") is valid`, *validMinimalIDP.Spec.GitHubAPI.Host), + buildLogForUpdatingTLSConfigurationValid("minimal-idp-name", "True", "Success", "spec.githubAPI.tls.certificateAuthorityData is valid"), + buildLogForUpdatingGitHubConnectionValid("minimal-idp-name", "True", "Success", `spec.githubAPI.host (\"%s\") is reachable and TLS verification succeeds`, *validMinimalIDP.Spec.GitHubAPI.Host), + buildLogForUpdatingPhase("minimal-idp-name", "Ready"), + }, + }, + { + name: "happy path using github.com", + secrets: []runtime.Object{goodSecret}, + githubIdentityProviders: []runtime.Object{ + func() runtime.Object { + githubIDP := validMinimalIDP.DeepCopy() + githubIDP.Spec.GitHubAPI.Host = ptr.To("github.com") + // don't change the CA because we are not really going to dial github.com in this test + return githubIDP + }(), + }, + mockDialer: func(network, addr string, config *tls.Config) (*tls.Conn, error) { + require.Equal(t, "github.com:443", addr) + // don't actually dial github.com to avoid making external network calls in unit test + certPool, _, err := pinnipedcontroller.BuildCertPoolIDP(validMinimalIDP.Spec.GitHubAPI.TLS) + require.NoError(t, err) + configClone := config.Clone() + configClone.RootCAs = certPool + return tls.Dial(network, goodServerDomain, configClone) + }, + wantResultingCache: []*upstreamgithub.ProviderConfig{ + { + Name: "minimal-idp-name", + ResourceUID: "minimal-uid", + APIBaseURL: "https://api.github.com", + UsernameAttribute: "login", + GroupNameAttribute: "slug", + OAuth2Config: &oauth2.Config{ + ClientID: "some-client-id", + ClientSecret: "some-client-secret", + Endpoint: oauth2.Endpoint{ + AuthURL: "https://github.com:443/login/oauth/authorize", + DeviceAuthURL: "", // not used + TokenURL: "https://github.com:443/login/oauth/access_token", + AuthStyle: oauth2.AuthStyleInParams, + }, + RedirectURL: "", // not used + Scopes: []string{"read:user", "read:org"}, + }, + AllowedOrganizations: setutil.NewCaseInsensitiveSet(), + HttpClient: nil, // let the test runner populate this for us + }, + }, + wantResultingUpstreams: []v1alpha1.GitHubIdentityProvider{ + { + ObjectMeta: validMinimalIDP.ObjectMeta, + Spec: func() v1alpha1.GitHubIdentityProviderSpec { + githubIDP := validMinimalIDP.DeepCopy() + githubIDP.Spec.GitHubAPI.Host = ptr.To("github.com") + // don't change the CA because we are not really going to dial github.com in this test + return githubIDP.Spec + }(), + Status: v1alpha1.GitHubIdentityProviderStatus{ + Phase: v1alpha1.GitHubPhaseReady, + Conditions: []metav1.Condition{ + buildClaimsValidatedTrue(t), + buildClientCredentialsSecretValidTrue(t, validMinimalIDP.Spec.Client.SecretName), + buildGitHubConnectionValidTrue(t, "github.com:443"), + buildHostValidTrue(t, "github.com"), + buildOrganizationsPolicyValidTrue(t, *validMinimalIDP.Spec.AllowAuthentication.Organizations.Policy), + buildTLSConfigurationValidTrue(t), + }, + }, + }, + }, + wantLogs: []string{ + buildLogForUpdatingClientCredentialsSecretValid("minimal-idp-name", "True", "Success", fmt.Sprintf(`clientID and clientSecret have been read from spec.client.SecretName (\"%s\")`, validMinimalIDP.Spec.Client.SecretName)), + buildLogForUpdatingClaimsValidTrue("minimal-idp-name"), + buildLogForUpdatingOrganizationPolicyValid("minimal-idp-name", "True", "Success", fmt.Sprintf(`spec.allowAuthentication.organizations.policy (\"%s\") is valid`, string(*validMinimalIDP.Spec.AllowAuthentication.Organizations.Policy))), + buildLogForUpdatingHostValid("minimal-idp-name", "True", "Success", `spec.githubAPI.host (\"%s\") is valid`, "github.com"), + buildLogForUpdatingTLSConfigurationValid("minimal-idp-name", "True", "Success", "spec.githubAPI.tls.certificateAuthorityData is valid"), + buildLogForUpdatingGitHubConnectionValid("minimal-idp-name", "True", "Success", `spec.githubAPI.host (\"%s\") is reachable and TLS verification succeeds`, "github.com:443"), + buildLogForUpdatingPhase("minimal-idp-name", "Ready"), + }, + }, + { + name: "happy path with IPv6", + secrets: []runtime.Object{goodSecret}, + githubIdentityProviders: []runtime.Object{ + func() runtime.Object { + ipv6IDP := validMinimalIDP.DeepCopy() + ipv6IDP.Spec.GitHubAPI.Host = ptr.To(goodServerIPv6Domain) + ipv6IDP.Spec.GitHubAPI.TLS = &v1alpha1.TLSSpec{ + CertificateAuthorityData: goodServerIPv6CAB64, + } + return ipv6IDP + }(), + }, + wantResultingCache: []*upstreamgithub.ProviderConfig{ + { + Name: "minimal-idp-name", + ResourceUID: "minimal-uid", + APIBaseURL: fmt.Sprintf("https://%s/api/v3", goodServerIPv6Domain), + UsernameAttribute: "login", + GroupNameAttribute: "slug", + OAuth2Config: &oauth2.Config{ + ClientID: "some-client-id", + ClientSecret: "some-client-secret", + Endpoint: oauth2.Endpoint{ + AuthURL: fmt.Sprintf("https://%s/login/oauth/authorize", goodServerIPv6Domain), + DeviceAuthURL: "", // not used + TokenURL: fmt.Sprintf("https://%s/login/oauth/access_token", goodServerIPv6Domain), + AuthStyle: oauth2.AuthStyleInParams, + }, + RedirectURL: "", // not used + Scopes: []string{"read:user", "read:org"}, + }, + AllowedOrganizations: setutil.NewCaseInsensitiveSet(), + HttpClient: nil, // let the test runner populate this for us + }, + }, + wantResultingUpstreams: []v1alpha1.GitHubIdentityProvider{ + { + ObjectMeta: validMinimalIDP.ObjectMeta, + Spec: func() v1alpha1.GitHubIdentityProviderSpec { + otherSpec := validMinimalIDP.Spec.DeepCopy() + otherSpec.GitHubAPI.Host = ptr.To(goodServerIPv6Domain) + otherSpec.GitHubAPI.TLS = &v1alpha1.TLSSpec{ + CertificateAuthorityData: goodServerIPv6CAB64, + } + return *otherSpec + }(), + + Status: v1alpha1.GitHubIdentityProviderStatus{ + Phase: v1alpha1.GitHubPhaseReady, + Conditions: []metav1.Condition{ + buildClaimsValidatedTrue(t), + buildClientCredentialsSecretValidTrue(t, validMinimalIDP.Spec.Client.SecretName), + buildGitHubConnectionValidTrue(t, goodServerIPv6Domain), + buildHostValidTrue(t, goodServerIPv6Domain), + buildOrganizationsPolicyValidTrue(t, *validMinimalIDP.Spec.AllowAuthentication.Organizations.Policy), + buildTLSConfigurationValidTrue(t), + }, + }, + }, + }, + wantLogs: []string{ + buildLogForUpdatingClientCredentialsSecretValid("minimal-idp-name", "True", "Success", fmt.Sprintf(`clientID and clientSecret have been read from spec.client.SecretName (\"%s\")`, validMinimalIDP.Spec.Client.SecretName)), + buildLogForUpdatingClaimsValidTrue("minimal-idp-name"), + buildLogForUpdatingOrganizationPolicyValid("minimal-idp-name", "True", "Success", fmt.Sprintf(`spec.allowAuthentication.organizations.policy (\"%s\") is valid`, string(*validMinimalIDP.Spec.AllowAuthentication.Organizations.Policy))), + buildLogForUpdatingHostValid("minimal-idp-name", "True", "Success", `spec.githubAPI.host (\"%s\") is valid`, goodServerIPv6Domain), + buildLogForUpdatingTLSConfigurationValid("minimal-idp-name", "True", "Success", "spec.githubAPI.tls.certificateAuthorityData is valid"), + buildLogForUpdatingGitHubConnectionValid("minimal-idp-name", "True", "Success", `spec.githubAPI.host (\"%s\") is reachable and TLS verification succeeds`, goodServerIPv6Domain), + buildLogForUpdatingPhase("minimal-idp-name", "Ready"), + }, + }, + { + name: "multiple idps - two good, one invalid", + secrets: []runtime.Object{ + goodSecret, + func() runtime.Object { + otherSecret := goodSecret.DeepCopy() + otherSecret.Name = "other-secret-name" + otherSecret.Data["clientID"] = []byte("other-client-id") + otherSecret.Data["clientSecret"] = []byte("other-client-secret") + return otherSecret + }(), + }, + githubIdentityProviders: []runtime.Object{ + validFilledOutIDP, + func() runtime.Object { + otherIDP := validFilledOutIDP.DeepCopy() + otherIDP.Name = "other-idp-name" + otherIDP.Spec.Client.SecretName = "other-secret-name" + + // No other test happens to that this particular value passes validation + otherIDP.Spec.Claims.Username = ptr.To(v1alpha1.GitHubUsernameLoginAndID) + return otherIDP + }(), + func() runtime.Object { + invalidIDP := validFilledOutIDP.DeepCopy() + invalidIDP.Name = "invalid-idp-name" + invalidIDP.Spec.Client.SecretName = "no-secret-with-this-name" + return invalidIDP + }(), + }, + wantResultingCache: []*upstreamgithub.ProviderConfig{ + { + Name: "some-idp-name", + ResourceUID: "some-resource-uid", + APIBaseURL: fmt.Sprintf("https://%s/api/v3", *validFilledOutIDP.Spec.GitHubAPI.Host), + UsernameAttribute: "id", + GroupNameAttribute: "name", + OAuth2Config: &oauth2.Config{ + ClientID: "some-client-id", + ClientSecret: "some-client-secret", + Endpoint: oauth2.Endpoint{ + AuthURL: fmt.Sprintf("https://%s/login/oauth/authorize", *validFilledOutIDP.Spec.GitHubAPI.Host), + DeviceAuthURL: "", // not used + TokenURL: fmt.Sprintf("https://%s/login/oauth/access_token", *validFilledOutIDP.Spec.GitHubAPI.Host), + AuthStyle: oauth2.AuthStyleInParams, + }, + RedirectURL: "", // not used + Scopes: []string{"read:user", "read:org"}, + }, + AllowedOrganizations: setutil.NewCaseInsensitiveSet("organization1", "org2"), + HttpClient: nil, // let the test runner populate this for us + }, + { + Name: "other-idp-name", + ResourceUID: "some-resource-uid", + APIBaseURL: fmt.Sprintf("https://%s/api/v3", *validFilledOutIDP.Spec.GitHubAPI.Host), + UsernameAttribute: "login:id", + GroupNameAttribute: "name", + OAuth2Config: &oauth2.Config{ + ClientID: "other-client-id", + ClientSecret: "other-client-secret", + Endpoint: oauth2.Endpoint{ + AuthURL: fmt.Sprintf("https://%s/login/oauth/authorize", *validFilledOutIDP.Spec.GitHubAPI.Host), + DeviceAuthURL: "", // not used + TokenURL: fmt.Sprintf("https://%s/login/oauth/access_token", *validFilledOutIDP.Spec.GitHubAPI.Host), + AuthStyle: oauth2.AuthStyleInParams, + }, + RedirectURL: "", // not used + Scopes: []string{"read:user", "read:org"}, + }, + AllowedOrganizations: setutil.NewCaseInsensitiveSet("organization1", "org2"), + HttpClient: nil, // let the test runner populate this for us + }, + }, + wantResultingUpstreams: []v1alpha1.GitHubIdentityProvider{ + { + ObjectMeta: func() metav1.ObjectMeta { + otherMeta := validFilledOutIDP.ObjectMeta.DeepCopy() + otherMeta.Name = "invalid-idp-name" + return *otherMeta + }(), + Spec: func() v1alpha1.GitHubIdentityProviderSpec { + otherSpec := validFilledOutIDP.Spec.DeepCopy() + otherSpec.Client.SecretName = "no-secret-with-this-name" + return *otherSpec + }(), + Status: v1alpha1.GitHubIdentityProviderStatus{ + Phase: v1alpha1.GitHubPhaseError, + Conditions: []metav1.Condition{ + buildClaimsValidatedTrue(t), + buildClientCredentialsSecretValidFalse( + t, + `secret "no-secret-with-this-name" not found`, + "no-secret-with-this-name", + namespace, + upstreamwatchers.ReasonNotFound, + ), + buildGitHubConnectionValidTrue(t, *validFilledOutIDP.Spec.GitHubAPI.Host), + buildHostValidTrue(t, *validFilledOutIDP.Spec.GitHubAPI.Host), + buildOrganizationsPolicyValidTrue(t, *validFilledOutIDP.Spec.AllowAuthentication.Organizations.Policy), + buildTLSConfigurationValidTrue(t), + }, + }, + }, + { + ObjectMeta: func() metav1.ObjectMeta { + otherMeta := validFilledOutIDP.ObjectMeta.DeepCopy() + otherMeta.Name = "other-idp-name" + return *otherMeta + }(), + Spec: func() v1alpha1.GitHubIdentityProviderSpec { + otherSpec := validFilledOutIDP.Spec.DeepCopy() + otherSpec.Client.SecretName = "other-secret-name" + otherSpec.Claims.Username = ptr.To(v1alpha1.GitHubUsernameLoginAndID) + return *otherSpec + }(), + Status: v1alpha1.GitHubIdentityProviderStatus{ + Phase: v1alpha1.GitHubPhaseReady, + Conditions: []metav1.Condition{ + buildClaimsValidatedTrue(t), + buildClientCredentialsSecretValidTrue(t, "other-secret-name"), + buildGitHubConnectionValidTrue(t, *validFilledOutIDP.Spec.GitHubAPI.Host), + buildHostValidTrue(t, *validFilledOutIDP.Spec.GitHubAPI.Host), + buildOrganizationsPolicyValidTrue(t, *validFilledOutIDP.Spec.AllowAuthentication.Organizations.Policy), + buildTLSConfigurationValidTrue(t), + }, + }, + }, + { + ObjectMeta: validFilledOutIDP.ObjectMeta, + Spec: validFilledOutIDP.Spec, + Status: v1alpha1.GitHubIdentityProviderStatus{ + Phase: v1alpha1.GitHubPhaseReady, + Conditions: []metav1.Condition{ + buildClaimsValidatedTrue(t), + buildClientCredentialsSecretValidTrue(t, validFilledOutIDP.Spec.Client.SecretName), + buildGitHubConnectionValidTrue(t, *validFilledOutIDP.Spec.GitHubAPI.Host), + buildHostValidTrue(t, *validFilledOutIDP.Spec.GitHubAPI.Host), + buildOrganizationsPolicyValidTrue(t, *validFilledOutIDP.Spec.AllowAuthentication.Organizations.Policy), + buildTLSConfigurationValidTrue(t), + }, + }, + }, + }, + wantLogs: []string{ + buildLogForUpdatingClientCredentialsSecretValid("invalid-idp-name", "False", "SecretNotFound", `secret \"no-secret-with-this-name\" not found: secret from spec.client.SecretName (\"no-secret-with-this-name\") must be found in namespace \"some-namespace\" with type \"secrets.pinniped.dev/github-client\" and keys \"clientID\" and \"clientSecret\"`), + buildLogForUpdatingClaimsValidTrue("invalid-idp-name"), + buildLogForUpdatingOrganizationPolicyValid("invalid-idp-name", "True", "Success", fmt.Sprintf(`spec.allowAuthentication.organizations.policy (\"%s\") is valid`, string(*validFilledOutIDP.Spec.AllowAuthentication.Organizations.Policy))), + buildLogForUpdatingHostValid("invalid-idp-name", "True", "Success", `spec.githubAPI.host (\"%s\") is valid`, *validFilledOutIDP.Spec.GitHubAPI.Host), + buildLogForUpdatingTLSConfigurationValid("invalid-idp-name", "True", "Success", "spec.githubAPI.tls.certificateAuthorityData is valid"), + buildLogForUpdatingGitHubConnectionValid("invalid-idp-name", "True", "Success", `spec.githubAPI.host (\"%s\") is reachable and TLS verification succeeds`, *validFilledOutIDP.Spec.GitHubAPI.Host), + buildLogForUpdatingPhase("invalid-idp-name", "Error"), + + buildLogForUpdatingClientCredentialsSecretValid("other-idp-name", "True", "Success", `clientID and clientSecret have been read from spec.client.SecretName (\"other-secret-name\")`), + buildLogForUpdatingClaimsValidTrue("other-idp-name"), + buildLogForUpdatingOrganizationPolicyValid("other-idp-name", "True", "Success", fmt.Sprintf(`spec.allowAuthentication.organizations.policy (\"%s\") is valid`, string(*validFilledOutIDP.Spec.AllowAuthentication.Organizations.Policy))), + buildLogForUpdatingHostValid("other-idp-name", "True", "Success", `spec.githubAPI.host (\"%s\") is valid`, *validFilledOutIDP.Spec.GitHubAPI.Host), + buildLogForUpdatingTLSConfigurationValid("other-idp-name", "True", "Success", "spec.githubAPI.tls.certificateAuthorityData is valid"), + buildLogForUpdatingGitHubConnectionValid("other-idp-name", "True", "Success", `spec.githubAPI.host (\"%s\") is reachable and TLS verification succeeds`, *validFilledOutIDP.Spec.GitHubAPI.Host), + buildLogForUpdatingPhase("other-idp-name", "Ready"), + + buildLogForUpdatingClientCredentialsSecretValid("some-idp-name", "True", "Success", fmt.Sprintf(`clientID and clientSecret have been read from spec.client.SecretName (\"%s\")`, validFilledOutIDP.Spec.Client.SecretName)), + buildLogForUpdatingClaimsValidTrue("some-idp-name"), + buildLogForUpdatingOrganizationPolicyValid("some-idp-name", "True", "Success", fmt.Sprintf(`spec.allowAuthentication.organizations.policy (\"%s\") is valid`, string(*validFilledOutIDP.Spec.AllowAuthentication.Organizations.Policy))), + buildLogForUpdatingHostValid("some-idp-name", "True", "Success", `spec.githubAPI.host (\"%s\") is valid`, *validFilledOutIDP.Spec.GitHubAPI.Host), + buildLogForUpdatingTLSConfigurationValid("some-idp-name", "True", "Success", "spec.githubAPI.tls.certificateAuthorityData is valid"), + buildLogForUpdatingGitHubConnectionValid("some-idp-name", "True", "Success", `spec.githubAPI.host (\"%s\") is reachable and TLS verification succeeds`, *validFilledOutIDP.Spec.GitHubAPI.Host), + buildLogForUpdatingPhase("some-idp-name", "Ready"), + }, + }, + { + name: "Host error - missing spec.githubAPI.host", + secrets: []runtime.Object{goodSecret}, + githubIdentityProviders: []runtime.Object{ + func() runtime.Object { + badIDP := validFilledOutIDP.DeepCopy() + badIDP.Spec.GitHubAPI.Host = nil + return badIDP + }(), + }, + wantResultingUpstreams: []v1alpha1.GitHubIdentityProvider{ + { + ObjectMeta: validFilledOutIDP.ObjectMeta, + Spec: func() v1alpha1.GitHubIdentityProviderSpec { + badSpec := validFilledOutIDP.Spec.DeepCopy() + badSpec.GitHubAPI.Host = nil + return *badSpec + }(), + Status: v1alpha1.GitHubIdentityProviderStatus{ + Phase: v1alpha1.GitHubPhaseError, + Conditions: []metav1.Condition{ + buildClaimsValidatedTrue(t), + buildClientCredentialsSecretValidTrue(t, validFilledOutIDP.Spec.Client.SecretName), + buildGitHubConnectionValidUnknown(t), + buildHostValidFalse(t, "", "must not be empty"), + buildOrganizationsPolicyValidTrue(t, *validFilledOutIDP.Spec.AllowAuthentication.Organizations.Policy), + buildTLSConfigurationValidTrue(t), + }, + }, + }, + }, + wantLogs: []string{ + buildLogForUpdatingClientCredentialsSecretValid("some-idp-name", "True", "Success", fmt.Sprintf(`clientID and clientSecret have been read from spec.client.SecretName (\"%s\")`, validFilledOutIDP.Spec.Client.SecretName)), + buildLogForUpdatingClaimsValidTrue("some-idp-name"), + buildLogForUpdatingOrganizationPolicyValid("some-idp-name", "True", "Success", fmt.Sprintf(`spec.allowAuthentication.organizations.policy (\"%s\") is valid`, string(*validFilledOutIDP.Spec.AllowAuthentication.Organizations.Policy))), + buildLogForUpdatingHostValid("some-idp-name", "False", "InvalidHost", `spec.githubAPI.host (\"%s\") is not valid: must not be empty`, ""), + buildLogForUpdatingTLSConfigurationValid("some-idp-name", "True", "Success", "spec.githubAPI.tls.certificateAuthorityData is valid"), + buildLogForUpdatingGitHubConnectionValidUnknown("some-idp-name"), + buildLogForUpdatingPhase("some-idp-name", "Error"), + }, + }, + { + name: "Host error - protocol/schema is specified", + secrets: []runtime.Object{goodSecret}, + githubIdentityProviders: []runtime.Object{ + func() runtime.Object { + badIDP := validMinimalIDP.DeepCopy() + badIDP.Spec.GitHubAPI.Host = ptr.To("https://example.com") + return badIDP + }(), + }, + wantResultingUpstreams: []v1alpha1.GitHubIdentityProvider{ + { + ObjectMeta: validMinimalIDP.ObjectMeta, + Spec: func() v1alpha1.GitHubIdentityProviderSpec { + badSpec := validMinimalIDP.Spec.DeepCopy() + badSpec.GitHubAPI.Host = ptr.To("https://example.com") + return *badSpec + }(), + Status: v1alpha1.GitHubIdentityProviderStatus{ + Phase: v1alpha1.GitHubPhaseError, + Conditions: []metav1.Condition{ + buildClaimsValidatedTrue(t), + buildClientCredentialsSecretValidTrue(t, validFilledOutIDP.Spec.Client.SecretName), + buildGitHubConnectionValidUnknown(t), + buildHostValidFalse(t, "https://example.com", `invalid port "//example.com"`), + buildOrganizationsPolicyValidTrue(t, *validMinimalIDP.Spec.AllowAuthentication.Organizations.Policy), + buildTLSConfigurationValidTrue(t), + }, + }, + }, + }, + wantLogs: []string{ + buildLogForUpdatingClientCredentialsSecretValid("minimal-idp-name", "True", "Success", fmt.Sprintf(`clientID and clientSecret have been read from spec.client.SecretName (\"%s\")`, validMinimalIDP.Spec.Client.SecretName)), + buildLogForUpdatingClaimsValidTrue("minimal-idp-name"), + buildLogForUpdatingOrganizationPolicyValid("minimal-idp-name", "True", "Success", fmt.Sprintf(`spec.allowAuthentication.organizations.policy (\"%s\") is valid`, string(*validMinimalIDP.Spec.AllowAuthentication.Organizations.Policy))), + buildLogForUpdatingHostValid("minimal-idp-name", "False", "InvalidHost", `spec.githubAPI.host (\"%s\") is not valid: invalid port \"//example.com\"`, "https://example.com"), + buildLogForUpdatingTLSConfigurationValid("minimal-idp-name", "True", "Success", "spec.githubAPI.tls.certificateAuthorityData is valid"), + buildLogForUpdatingGitHubConnectionValidUnknown("minimal-idp-name"), + buildLogForUpdatingPhase("minimal-idp-name", "Error"), + }, + }, + { + name: "Host error - path is specified", + secrets: []runtime.Object{goodSecret}, + githubIdentityProviders: []runtime.Object{ + func() runtime.Object { + badIDP := validMinimalIDP.DeepCopy() + badIDP.Spec.GitHubAPI.Host = ptr.To("example.com/foo") + return badIDP + }(), + }, + wantResultingUpstreams: []v1alpha1.GitHubIdentityProvider{ + { + ObjectMeta: validMinimalIDP.ObjectMeta, + Spec: func() v1alpha1.GitHubIdentityProviderSpec { + badSpec := validMinimalIDP.Spec.DeepCopy() + badSpec.GitHubAPI.Host = ptr.To("example.com/foo") + return *badSpec + }(), + Status: v1alpha1.GitHubIdentityProviderStatus{ + Phase: v1alpha1.GitHubPhaseError, + Conditions: []metav1.Condition{ + buildClaimsValidatedTrue(t), + buildClientCredentialsSecretValidTrue(t, validMinimalIDP.Spec.Client.SecretName), + buildGitHubConnectionValidUnknown(t), + buildHostValidFalse(t, "example.com/foo", `host "example.com/foo" is not a valid hostname or IP address`), + buildOrganizationsPolicyValidTrue(t, *validMinimalIDP.Spec.AllowAuthentication.Organizations.Policy), + buildTLSConfigurationValidTrue(t), + }, + }, + }, + }, + wantLogs: []string{ + buildLogForUpdatingClientCredentialsSecretValid("minimal-idp-name", "True", "Success", fmt.Sprintf(`clientID and clientSecret have been read from spec.client.SecretName (\"%s\")`, validMinimalIDP.Spec.Client.SecretName)), + buildLogForUpdatingClaimsValidTrue("minimal-idp-name"), + buildLogForUpdatingOrganizationPolicyValid("minimal-idp-name", "True", "Success", fmt.Sprintf(`spec.allowAuthentication.organizations.policy (\"%s\") is valid`, string(*validMinimalIDP.Spec.AllowAuthentication.Organizations.Policy))), + buildLogForUpdatingHostValid("minimal-idp-name", "False", "InvalidHost", `spec.githubAPI.host (\"%s\") is not valid: host \"example.com/foo\" is not a valid hostname or IP address`, "example.com/foo"), + buildLogForUpdatingTLSConfigurationValid("minimal-idp-name", "True", "Success", "spec.githubAPI.tls.certificateAuthorityData is valid"), + buildLogForUpdatingGitHubConnectionValidUnknown("minimal-idp-name"), + buildLogForUpdatingPhase("minimal-idp-name", "Error"), + }, + }, + { + name: "Host error - userinfo is specified", + secrets: []runtime.Object{goodSecret}, + githubIdentityProviders: []runtime.Object{ + func() runtime.Object { + badIDP := validMinimalIDP.DeepCopy() + badIDP.Spec.GitHubAPI.Host = ptr.To("u:p@example.com") + return badIDP + }(), + }, + wantResultingUpstreams: []v1alpha1.GitHubIdentityProvider{ + { + ObjectMeta: validMinimalIDP.ObjectMeta, + Spec: func() v1alpha1.GitHubIdentityProviderSpec { + badSpec := validMinimalIDP.Spec.DeepCopy() + badSpec.GitHubAPI.Host = ptr.To("u:p@example.com") + return *badSpec + }(), + Status: v1alpha1.GitHubIdentityProviderStatus{ + Phase: v1alpha1.GitHubPhaseError, + Conditions: []metav1.Condition{ + buildClaimsValidatedTrue(t), + buildClientCredentialsSecretValidTrue(t, validMinimalIDP.Spec.Client.SecretName), + buildGitHubConnectionValidUnknown(t), + buildHostValidFalse(t, "u:p@example.com", `invalid port "p@example.com"`), + buildOrganizationsPolicyValidTrue(t, *validMinimalIDP.Spec.AllowAuthentication.Organizations.Policy), + buildTLSConfigurationValidTrue(t), + }, + }, + }, + }, + wantLogs: []string{ + buildLogForUpdatingClientCredentialsSecretValid("minimal-idp-name", "True", "Success", fmt.Sprintf(`clientID and clientSecret have been read from spec.client.SecretName (\"%s\")`, validMinimalIDP.Spec.Client.SecretName)), + buildLogForUpdatingClaimsValidTrue("minimal-idp-name"), + buildLogForUpdatingOrganizationPolicyValid("minimal-idp-name", "True", "Success", fmt.Sprintf(`spec.allowAuthentication.organizations.policy (\"%s\") is valid`, string(*validMinimalIDP.Spec.AllowAuthentication.Organizations.Policy))), + buildLogForUpdatingHostValid("minimal-idp-name", "False", "InvalidHost", `spec.githubAPI.host (\"%s\") is not valid: invalid port \"p@example.com\"`, "u:p@example.com"), + buildLogForUpdatingTLSConfigurationValid("minimal-idp-name", "True", "Success", "spec.githubAPI.tls.certificateAuthorityData is valid"), + buildLogForUpdatingGitHubConnectionValidUnknown("minimal-idp-name"), + buildLogForUpdatingPhase("minimal-idp-name", "Error"), + }, + }, + { + name: "Host error - query is specified", + secrets: []runtime.Object{goodSecret}, + githubIdentityProviders: []runtime.Object{ + func() runtime.Object { + badIDP := validMinimalIDP.DeepCopy() + badIDP.Spec.GitHubAPI.Host = ptr.To("example.com?a=b") + return badIDP + }(), + }, + wantResultingUpstreams: []v1alpha1.GitHubIdentityProvider{ + { + ObjectMeta: validMinimalIDP.ObjectMeta, + Spec: func() v1alpha1.GitHubIdentityProviderSpec { + badSpec := validMinimalIDP.Spec.DeepCopy() + badSpec.GitHubAPI.Host = ptr.To("example.com?a=b") + return *badSpec + }(), + Status: v1alpha1.GitHubIdentityProviderStatus{ + Phase: v1alpha1.GitHubPhaseError, + Conditions: []metav1.Condition{ + buildClaimsValidatedTrue(t), + buildClientCredentialsSecretValidTrue(t, validMinimalIDP.Spec.Client.SecretName), + buildGitHubConnectionValidUnknown(t), + buildHostValidFalse(t, "example.com?a=b", `host "example.com?a=b" is not a valid hostname or IP address`), + buildOrganizationsPolicyValidTrue(t, *validMinimalIDP.Spec.AllowAuthentication.Organizations.Policy), + buildTLSConfigurationValidTrue(t), + }, + }, + }, + }, + wantLogs: []string{ + buildLogForUpdatingClientCredentialsSecretValid("minimal-idp-name", "True", "Success", fmt.Sprintf(`clientID and clientSecret have been read from spec.client.SecretName (\"%s\")`, validMinimalIDP.Spec.Client.SecretName)), + buildLogForUpdatingClaimsValidTrue("minimal-idp-name"), + buildLogForUpdatingOrganizationPolicyValid("minimal-idp-name", "True", "Success", fmt.Sprintf(`spec.allowAuthentication.organizations.policy (\"%s\") is valid`, string(*validMinimalIDP.Spec.AllowAuthentication.Organizations.Policy))), + buildLogForUpdatingHostValid("minimal-idp-name", "False", "InvalidHost", `spec.githubAPI.host (\"%s\") is not valid: host \"example.com?a=b\" is not a valid hostname or IP address`, "example.com?a=b"), + buildLogForUpdatingTLSConfigurationValid("minimal-idp-name", "True", "Success", "spec.githubAPI.tls.certificateAuthorityData is valid"), + buildLogForUpdatingGitHubConnectionValidUnknown("minimal-idp-name"), + buildLogForUpdatingPhase("minimal-idp-name", "Error"), + }, + }, + { + name: "Host error - fragment is specified", + secrets: []runtime.Object{goodSecret}, + githubIdentityProviders: []runtime.Object{ + func() runtime.Object { + badIDP := validMinimalIDP.DeepCopy() + badIDP.Spec.GitHubAPI.Host = ptr.To("example.com#a") + return badIDP + }(), + }, + wantResultingUpstreams: []v1alpha1.GitHubIdentityProvider{ + { + ObjectMeta: validMinimalIDP.ObjectMeta, + Spec: func() v1alpha1.GitHubIdentityProviderSpec { + badSpec := validMinimalIDP.Spec.DeepCopy() + badSpec.GitHubAPI.Host = ptr.To("example.com#a") + return *badSpec + }(), + Status: v1alpha1.GitHubIdentityProviderStatus{ + Phase: v1alpha1.GitHubPhaseError, + Conditions: []metav1.Condition{ + buildClaimsValidatedTrue(t), + buildClientCredentialsSecretValidTrue(t, validMinimalIDP.Spec.Client.SecretName), + buildGitHubConnectionValidUnknown(t), + buildHostValidFalse(t, "example.com#a", `host "example.com#a" is not a valid hostname or IP address`), + buildOrganizationsPolicyValidTrue(t, *validMinimalIDP.Spec.AllowAuthentication.Organizations.Policy), + buildTLSConfigurationValidTrue(t), + }, + }, + }, + }, + wantLogs: []string{ + buildLogForUpdatingClientCredentialsSecretValid("minimal-idp-name", "True", "Success", fmt.Sprintf(`clientID and clientSecret have been read from spec.client.SecretName (\"%s\")`, validMinimalIDP.Spec.Client.SecretName)), + buildLogForUpdatingClaimsValidTrue("minimal-idp-name"), + buildLogForUpdatingOrganizationPolicyValid("minimal-idp-name", "True", "Success", fmt.Sprintf(`spec.allowAuthentication.organizations.policy (\"%s\") is valid`, string(*validMinimalIDP.Spec.AllowAuthentication.Organizations.Policy))), + buildLogForUpdatingHostValid("minimal-idp-name", "False", "InvalidHost", `spec.githubAPI.host (\"%s\") is not valid: host \"example.com#a\" is not a valid hostname or IP address`, "example.com#a"), + buildLogForUpdatingTLSConfigurationValid("minimal-idp-name", "True", "Success", "spec.githubAPI.tls.certificateAuthorityData is valid"), + buildLogForUpdatingGitHubConnectionValidUnknown("minimal-idp-name"), + buildLogForUpdatingPhase("minimal-idp-name", "Error"), + }, + }, + { + name: "TLS error - invalid bundle", + secrets: []runtime.Object{goodSecret}, + githubIdentityProviders: []runtime.Object{ + func() runtime.Object { + badIDP := validFilledOutIDP.DeepCopy() + badIDP.Spec.GitHubAPI.TLS = &v1alpha1.TLSSpec{ + CertificateAuthorityData: base64.StdEncoding.EncodeToString([]byte("foo")), + } + return badIDP + }(), + }, + wantResultingUpstreams: []v1alpha1.GitHubIdentityProvider{ + { + ObjectMeta: validFilledOutIDP.ObjectMeta, + Spec: func() v1alpha1.GitHubIdentityProviderSpec { + badSpec := validFilledOutIDP.Spec.DeepCopy() + badSpec.GitHubAPI.TLS = &v1alpha1.TLSSpec{ + CertificateAuthorityData: base64.StdEncoding.EncodeToString([]byte("foo")), + } + return *badSpec + }(), + Status: v1alpha1.GitHubIdentityProviderStatus{ + Phase: v1alpha1.GitHubPhaseError, + Conditions: []metav1.Condition{ + buildClaimsValidatedTrue(t), + buildClientCredentialsSecretValidTrue(t, validFilledOutIDP.Spec.Client.SecretName), + buildGitHubConnectionValidUnknown(t), + buildHostValidTrue(t, *validFilledOutIDP.Spec.GitHubAPI.Host), + buildOrganizationsPolicyValidTrue(t, *validFilledOutIDP.Spec.AllowAuthentication.Organizations.Policy), + buildTLSConfigurationValidFalse(t, "spec.githubAPI.tls.certificateAuthorityData is not valid: certificateAuthorityData is not valid PEM: data does not contain any valid RSA or ECDSA certificates"), + }, + }, + }, + }, + wantLogs: []string{ + buildLogForUpdatingClientCredentialsSecretValid("some-idp-name", "True", "Success", fmt.Sprintf(`clientID and clientSecret have been read from spec.client.SecretName (\"%s\")`, validFilledOutIDP.Spec.Client.SecretName)), + buildLogForUpdatingClaimsValidTrue("some-idp-name"), + buildLogForUpdatingOrganizationPolicyValid("some-idp-name", "True", "Success", fmt.Sprintf(`spec.allowAuthentication.organizations.policy (\"%s\") is valid`, string(*validFilledOutIDP.Spec.AllowAuthentication.Organizations.Policy))), + buildLogForUpdatingHostValid("some-idp-name", "True", "Success", `spec.githubAPI.host (\"%s\") is valid`, *validFilledOutIDP.Spec.GitHubAPI.Host), + buildLogForUpdatingTLSConfigurationValid("some-idp-name", "False", "InvalidTLSConfig", "spec.githubAPI.tls.certificateAuthorityData is not valid: certificateAuthorityData is not valid PEM: data does not contain any valid RSA or ECDSA certificates"), + buildLogForUpdatingGitHubConnectionValidUnknown("some-idp-name"), + buildLogForUpdatingPhase("some-idp-name", "Error"), + }, + }, + { + name: "Connection error - no such host", + secrets: []runtime.Object{goodSecret}, + githubIdentityProviders: []runtime.Object{ + func() runtime.Object { + badIDP := validMinimalIDP.DeepCopy() + badIDP.Spec.GitHubAPI.Host = ptr.To("nowhere.bad-tld") + return badIDP + }(), + }, + wantErr: "dial tcp: lookup nowhere.bad-tld: no such host", + wantResultingUpstreams: []v1alpha1.GitHubIdentityProvider{ + { + ObjectMeta: validMinimalIDP.ObjectMeta, + Spec: func() v1alpha1.GitHubIdentityProviderSpec { + badSpec := validMinimalIDP.Spec.DeepCopy() + badSpec.GitHubAPI.Host = ptr.To("nowhere.bad-tld") + return *badSpec + }(), + Status: v1alpha1.GitHubIdentityProviderStatus{ + Phase: v1alpha1.GitHubPhaseError, + Conditions: []metav1.Condition{ + buildClaimsValidatedTrue(t), + buildClientCredentialsSecretValidTrue(t, validMinimalIDP.Spec.Client.SecretName), + buildGitHubConnectionValidFalse(t, fmt.Sprintf(`cannot dial server spec.githubAPI.host (%q): dial tcp: lookup nowhere.bad-tld: no such host`, "nowhere.bad-tld:443")), + buildHostValidTrue(t, "nowhere.bad-tld"), + buildOrganizationsPolicyValidTrue(t, *validMinimalIDP.Spec.AllowAuthentication.Organizations.Policy), + buildTLSConfigurationValidTrue(t), + }, + }, + }, + }, + wantLogs: []string{ + buildLogForUpdatingClientCredentialsSecretValid("minimal-idp-name", "True", "Success", fmt.Sprintf(`clientID and clientSecret have been read from spec.client.SecretName (\"%s\")`, validMinimalIDP.Spec.Client.SecretName)), + buildLogForUpdatingClaimsValidTrue("minimal-idp-name"), + buildLogForUpdatingOrganizationPolicyValid("minimal-idp-name", "True", "Success", fmt.Sprintf(`spec.allowAuthentication.organizations.policy (\"%s\") is valid`, string(*validMinimalIDP.Spec.AllowAuthentication.Organizations.Policy))), + buildLogForUpdatingHostValid("minimal-idp-name", "True", "Success", `spec.githubAPI.host (\"%s\") is valid`, "nowhere.bad-tld"), + buildLogForUpdatingTLSConfigurationValid("minimal-idp-name", "True", "Success", "spec.githubAPI.tls.certificateAuthorityData is valid"), + buildLogForUpdatingGitHubConnectionValid("minimal-idp-name", "False", "UnableToDialServer", `cannot dial server spec.githubAPI.host (\"%s\"): dial tcp: lookup nowhere.bad-tld: no such host`, "nowhere.bad-tld:443"), + buildLogForUpdatingPhase("minimal-idp-name", "Error"), + }, + }, + { + name: "Connection error - ipv6 without brackets", + secrets: []runtime.Object{goodSecret}, + githubIdentityProviders: []runtime.Object{ + func() runtime.Object { + badIDP := validMinimalIDP.DeepCopy() + badIDP.Spec.GitHubAPI.Host = ptr.To("0:0:0:0:0:0:0:1:9876") + return badIDP + }(), + }, + wantResultingUpstreams: []v1alpha1.GitHubIdentityProvider{ + { + ObjectMeta: validMinimalIDP.ObjectMeta, + Spec: func() v1alpha1.GitHubIdentityProviderSpec { + badSpec := validMinimalIDP.Spec.DeepCopy() + badSpec.GitHubAPI.Host = ptr.To("0:0:0:0:0:0:0:1:9876") + return *badSpec + }(), + Status: v1alpha1.GitHubIdentityProviderStatus{ + Phase: v1alpha1.GitHubPhaseError, + Conditions: []metav1.Condition{ + buildClaimsValidatedTrue(t), + buildClientCredentialsSecretValidTrue(t, validMinimalIDP.Spec.Client.SecretName), + buildGitHubConnectionValidUnknown(t), + buildHostValidFalse(t, "0:0:0:0:0:0:0:1:9876", `host "0:0:0:0:0:0:0:1:9876" is not a valid hostname or IP address`), + buildOrganizationsPolicyValidTrue(t, *validMinimalIDP.Spec.AllowAuthentication.Organizations.Policy), + buildTLSConfigurationValidTrue(t), + }, + }, + }, + }, + wantLogs: []string{ + buildLogForUpdatingClientCredentialsSecretValid("minimal-idp-name", "True", "Success", fmt.Sprintf(`clientID and clientSecret have been read from spec.client.SecretName (\"%s\")`, validMinimalIDP.Spec.Client.SecretName)), + buildLogForUpdatingClaimsValidTrue("minimal-idp-name"), + buildLogForUpdatingOrganizationPolicyValid("minimal-idp-name", "True", "Success", fmt.Sprintf(`spec.allowAuthentication.organizations.policy (\"%s\") is valid`, string(*validMinimalIDP.Spec.AllowAuthentication.Organizations.Policy))), + buildLogForUpdatingHostValid("minimal-idp-name", "False", "InvalidHost", `spec.githubAPI.host (\"0:0:0:0:0:0:0:1:9876\") is not valid: host \"%s\" is not a valid hostname or IP address`, "0:0:0:0:0:0:0:1:9876"), + buildLogForUpdatingTLSConfigurationValid("minimal-idp-name", "True", "Success", "spec.githubAPI.tls.certificateAuthorityData is valid"), + buildLogForUpdatingGitHubConnectionValidUnknown("minimal-idp-name"), + buildLogForUpdatingPhase("minimal-idp-name", "Error"), + }, + }, + { + name: "Connection error - host not trusted by system certs", + secrets: []runtime.Object{goodSecret}, + githubIdentityProviders: []runtime.Object{ + func() runtime.Object { + badIDP := validFilledOutIDP.DeepCopy() + badIDP.Spec.GitHubAPI.TLS = nil + return badIDP + }(), + }, + wantErr: "tls: failed to verify certificate: x509: certificate signed by unknown authority", + wantResultingUpstreams: []v1alpha1.GitHubIdentityProvider{ + { + ObjectMeta: validFilledOutIDP.ObjectMeta, + Spec: func() v1alpha1.GitHubIdentityProviderSpec { + badSpec := validFilledOutIDP.Spec.DeepCopy() + badSpec.GitHubAPI.TLS = nil + return *badSpec + }(), + Status: v1alpha1.GitHubIdentityProviderStatus{ + Phase: v1alpha1.GitHubPhaseError, + Conditions: []metav1.Condition{ + buildClaimsValidatedTrue(t), + buildClientCredentialsSecretValidTrue(t, validFilledOutIDP.Spec.Client.SecretName), + buildGitHubConnectionValidFalse(t, fmt.Sprintf(`cannot dial server spec.githubAPI.host (%q): tls: failed to verify certificate: x509: certificate signed by unknown authority`, *validFilledOutIDP.Spec.GitHubAPI.Host)), + buildHostValidTrue(t, *validFilledOutIDP.Spec.GitHubAPI.Host), + buildOrganizationsPolicyValidTrue(t, *validFilledOutIDP.Spec.AllowAuthentication.Organizations.Policy), + buildTLSConfigurationValidTrue(t), + }, + }, + }, + }, + wantLogs: []string{ + buildLogForUpdatingClientCredentialsSecretValid("some-idp-name", "True", "Success", fmt.Sprintf(`clientID and clientSecret have been read from spec.client.SecretName (\"%s\")`, validFilledOutIDP.Spec.Client.SecretName)), + buildLogForUpdatingClaimsValidTrue("some-idp-name"), + buildLogForUpdatingOrganizationPolicyValid("some-idp-name", "True", "Success", fmt.Sprintf(`spec.allowAuthentication.organizations.policy (\"%s\") is valid`, string(*validFilledOutIDP.Spec.AllowAuthentication.Organizations.Policy))), + buildLogForUpdatingHostValid("some-idp-name", "True", "Success", `spec.githubAPI.host (\"%s\") is valid`, *validFilledOutIDP.Spec.GitHubAPI.Host), + buildLogForUpdatingTLSConfigurationValid("some-idp-name", "True", "Success", "spec.githubAPI.tls.certificateAuthorityData is valid"), + buildLogForUpdatingGitHubConnectionValid("some-idp-name", "False", "UnableToDialServer", `cannot dial server spec.githubAPI.host (\"%s\"): tls: failed to verify certificate: x509: certificate signed by unknown authority`, *validFilledOutIDP.Spec.GitHubAPI.Host), + buildLogForUpdatingPhase("some-idp-name", "Error"), + }, + }, + { + name: "Connection error - host not trusted by provided CA bundle", + secrets: []runtime.Object{goodSecret}, + githubIdentityProviders: []runtime.Object{ + func() runtime.Object { + badIDP := validFilledOutIDP.DeepCopy() + badIDP.Spec.GitHubAPI.TLS = &v1alpha1.TLSSpec{ + CertificateAuthorityData: base64.StdEncoding.EncodeToString(unknownServerCABytes), + } + return badIDP + }(), + }, + wantErr: "tls: failed to verify certificate: x509: certificate signed by unknown authority", + wantResultingUpstreams: []v1alpha1.GitHubIdentityProvider{ + { + ObjectMeta: validFilledOutIDP.ObjectMeta, + Spec: func() v1alpha1.GitHubIdentityProviderSpec { + badSpec := validFilledOutIDP.Spec.DeepCopy() + badSpec.GitHubAPI.TLS = &v1alpha1.TLSSpec{ + CertificateAuthorityData: base64.StdEncoding.EncodeToString(unknownServerCABytes), + } + return *badSpec + }(), + Status: v1alpha1.GitHubIdentityProviderStatus{ + Phase: v1alpha1.GitHubPhaseError, + Conditions: []metav1.Condition{ + buildClaimsValidatedTrue(t), + buildClientCredentialsSecretValidTrue(t, validFilledOutIDP.Spec.Client.SecretName), + buildGitHubConnectionValidFalse(t, fmt.Sprintf(`cannot dial server spec.githubAPI.host (%q): tls: failed to verify certificate: x509: certificate signed by unknown authority`, *validFilledOutIDP.Spec.GitHubAPI.Host)), + buildHostValidTrue(t, *validFilledOutIDP.Spec.GitHubAPI.Host), + buildOrganizationsPolicyValidTrue(t, *validFilledOutIDP.Spec.AllowAuthentication.Organizations.Policy), + buildTLSConfigurationValidTrue(t), + }, + }, + }, + }, + wantLogs: []string{ + buildLogForUpdatingClientCredentialsSecretValid("some-idp-name", "True", "Success", fmt.Sprintf(`clientID and clientSecret have been read from spec.client.SecretName (\"%s\")`, validFilledOutIDP.Spec.Client.SecretName)), + buildLogForUpdatingClaimsValidTrue("some-idp-name"), + buildLogForUpdatingOrganizationPolicyValid("some-idp-name", "True", "Success", fmt.Sprintf(`spec.allowAuthentication.organizations.policy (\"%s\") is valid`, string(*validFilledOutIDP.Spec.AllowAuthentication.Organizations.Policy))), + buildLogForUpdatingHostValid("some-idp-name", "True", "Success", `spec.githubAPI.host (\"%s\") is valid`, *validFilledOutIDP.Spec.GitHubAPI.Host), + buildLogForUpdatingTLSConfigurationValid("some-idp-name", "True", "Success", "spec.githubAPI.tls.certificateAuthorityData is valid"), + buildLogForUpdatingGitHubConnectionValid("some-idp-name", "False", "UnableToDialServer", `cannot dial server spec.githubAPI.host (\"%s\"): tls: failed to verify certificate: x509: certificate signed by unknown authority`, *validFilledOutIDP.Spec.GitHubAPI.Host), + buildLogForUpdatingPhase("some-idp-name", "Error"), + }, + }, + { + name: "Organization Policy error - missing spec.allowAuthentication.organizations.policy", + secrets: []runtime.Object{goodSecret}, + githubIdentityProviders: []runtime.Object{ + func() runtime.Object { + badIDP := validFilledOutIDP.DeepCopy() + badIDP.Spec.AllowAuthentication.Organizations.Policy = nil + return badIDP + }(), + }, + wantResultingUpstreams: []v1alpha1.GitHubIdentityProvider{ + { + ObjectMeta: validFilledOutIDP.ObjectMeta, + Spec: func() v1alpha1.GitHubIdentityProviderSpec { + badSpec := validFilledOutIDP.Spec.DeepCopy() + badSpec.AllowAuthentication.Organizations.Policy = nil + return *badSpec + }(), + Status: v1alpha1.GitHubIdentityProviderStatus{ + Phase: v1alpha1.GitHubPhaseError, + Conditions: []metav1.Condition{ + buildClaimsValidatedTrue(t), + buildClientCredentialsSecretValidTrue(t, validFilledOutIDP.Spec.Client.SecretName), + buildGitHubConnectionValidTrue(t, *validFilledOutIDP.Spec.GitHubAPI.Host), + buildHostValidTrue(t, *validFilledOutIDP.Spec.GitHubAPI.Host), + buildOrganizationsPolicyValidFalse(t, "spec.allowAuthentication.organizations.policy must be 'OnlyUsersFromAllowedOrganizations' when spec.allowAuthentication.organizations.allowed has organizations listed"), + buildTLSConfigurationValidTrue(t), + }, + }, + }, + }, + wantLogs: []string{ + buildLogForUpdatingClientCredentialsSecretValid("some-idp-name", "True", "Success", fmt.Sprintf(`clientID and clientSecret have been read from spec.client.SecretName (\"%s\")`, validFilledOutIDP.Spec.Client.SecretName)), + buildLogForUpdatingClaimsValidTrue("some-idp-name"), + buildLogForUpdatingOrganizationPolicyValid("some-idp-name", "False", "Invalid", "spec.allowAuthentication.organizations.policy must be 'OnlyUsersFromAllowedOrganizations' when spec.allowAuthentication.organizations.allowed has organizations listed"), + buildLogForUpdatingHostValid("some-idp-name", "True", "Success", `spec.githubAPI.host (\"%s\") is valid`, *validFilledOutIDP.Spec.GitHubAPI.Host), + buildLogForUpdatingTLSConfigurationValid("some-idp-name", "True", "Success", "spec.githubAPI.tls.certificateAuthorityData is valid"), + buildLogForUpdatingGitHubConnectionValid("some-idp-name", "True", "Success", `spec.githubAPI.host (\"%s\") is reachable and TLS verification succeeds`, *validFilledOutIDP.Spec.GitHubAPI.Host), + buildLogForUpdatingPhase("some-idp-name", "Error"), + }, + }, + { + name: "Organization Policy error - invalid spec.allowAuthentication.organizations.policy", + secrets: []runtime.Object{goodSecret}, + githubIdentityProviders: []runtime.Object{ + func() runtime.Object { + badIDP := validFilledOutIDP.DeepCopy() + badIDP.Spec.AllowAuthentication.Organizations.Policy = ptr.To[v1alpha1.GitHubAllowedAuthOrganizationsPolicy]("a") + return badIDP + }(), + }, + wantResultingUpstreams: []v1alpha1.GitHubIdentityProvider{ + { + ObjectMeta: validFilledOutIDP.ObjectMeta, + Spec: func() v1alpha1.GitHubIdentityProviderSpec { + badSpec := validFilledOutIDP.Spec.DeepCopy() + badSpec.AllowAuthentication.Organizations.Policy = ptr.To[v1alpha1.GitHubAllowedAuthOrganizationsPolicy]("a") + return *badSpec + }(), + Status: v1alpha1.GitHubIdentityProviderStatus{ + Phase: v1alpha1.GitHubPhaseError, + Conditions: []metav1.Condition{ + buildClaimsValidatedTrue(t), + buildClientCredentialsSecretValidTrue(t, validFilledOutIDP.Spec.Client.SecretName), + buildGitHubConnectionValidTrue(t, *validFilledOutIDP.Spec.GitHubAPI.Host), + buildHostValidTrue(t, *validFilledOutIDP.Spec.GitHubAPI.Host), + buildOrganizationsPolicyValidFalse(t, "spec.allowAuthentication.organizations.policy must be 'OnlyUsersFromAllowedOrganizations' when spec.allowAuthentication.organizations.allowed has organizations listed"), + buildTLSConfigurationValidTrue(t), + }, + }, + }, + }, + wantLogs: []string{ + buildLogForUpdatingClientCredentialsSecretValid("some-idp-name", "True", "Success", fmt.Sprintf(`clientID and clientSecret have been read from spec.client.SecretName (\"%s\")`, validFilledOutIDP.Spec.Client.SecretName)), + buildLogForUpdatingClaimsValidTrue("some-idp-name"), + buildLogForUpdatingOrganizationPolicyValid("some-idp-name", "False", "Invalid", "spec.allowAuthentication.organizations.policy must be 'OnlyUsersFromAllowedOrganizations' when spec.allowAuthentication.organizations.allowed has organizations listed"), + buildLogForUpdatingHostValid("some-idp-name", "True", "Success", `spec.githubAPI.host (\"%s\") is valid`, *validFilledOutIDP.Spec.GitHubAPI.Host), + buildLogForUpdatingTLSConfigurationValid("some-idp-name", "True", "Success", "spec.githubAPI.tls.certificateAuthorityData is valid"), + buildLogForUpdatingGitHubConnectionValid("some-idp-name", "True", "Success", `spec.githubAPI.host (\"%s\") is reachable and TLS verification succeeds`, *validFilledOutIDP.Spec.GitHubAPI.Host), + buildLogForUpdatingPhase("some-idp-name", "Error"), + }, + }, + { + name: "Organization Policy error - spec.allowAuthentication.organizations.policy must be 'OnlyUsersFromAllowedOrganizations' when spec.allowAuthentication.organizations.allowed has organizations listed", + secrets: []runtime.Object{goodSecret}, + githubIdentityProviders: []runtime.Object{ + func() runtime.Object { + badIDP := validFilledOutIDP.DeepCopy() + badIDP.Spec.AllowAuthentication.Organizations.Policy = ptr.To(v1alpha1.GitHubAllowedAuthOrganizationsPolicyAllGitHubUsers) + return badIDP + }(), + }, + wantResultingUpstreams: []v1alpha1.GitHubIdentityProvider{ + { + ObjectMeta: validFilledOutIDP.ObjectMeta, + Spec: func() v1alpha1.GitHubIdentityProviderSpec { + badSpec := validFilledOutIDP.Spec.DeepCopy() + badSpec.AllowAuthentication.Organizations.Policy = ptr.To(v1alpha1.GitHubAllowedAuthOrganizationsPolicyAllGitHubUsers) + return *badSpec + }(), + Status: v1alpha1.GitHubIdentityProviderStatus{ + Phase: v1alpha1.GitHubPhaseError, + Conditions: []metav1.Condition{ + buildClaimsValidatedTrue(t), + buildClientCredentialsSecretValidTrue(t, validFilledOutIDP.Spec.Client.SecretName), + buildGitHubConnectionValidTrue(t, *validFilledOutIDP.Spec.GitHubAPI.Host), + buildHostValidTrue(t, *validFilledOutIDP.Spec.GitHubAPI.Host), + buildOrganizationsPolicyValidFalse(t, "spec.allowAuthentication.organizations.policy must be 'OnlyUsersFromAllowedOrganizations' when spec.allowAuthentication.organizations.allowed has organizations listed"), + buildTLSConfigurationValidTrue(t), + }, + }, + }, + }, + wantLogs: []string{ + buildLogForUpdatingClientCredentialsSecretValid("some-idp-name", "True", "Success", fmt.Sprintf(`clientID and clientSecret have been read from spec.client.SecretName (\"%s\")`, validFilledOutIDP.Spec.Client.SecretName)), + buildLogForUpdatingClaimsValidTrue("some-idp-name"), + buildLogForUpdatingOrganizationPolicyValid("some-idp-name", "False", "Invalid", "spec.allowAuthentication.organizations.policy must be 'OnlyUsersFromAllowedOrganizations' when spec.allowAuthentication.organizations.allowed has organizations listed"), + buildLogForUpdatingHostValid("some-idp-name", "True", "Success", `spec.githubAPI.host (\"%s\") is valid`, *validFilledOutIDP.Spec.GitHubAPI.Host), + buildLogForUpdatingTLSConfigurationValid("some-idp-name", "True", "Success", "spec.githubAPI.tls.certificateAuthorityData is valid"), + buildLogForUpdatingGitHubConnectionValid("some-idp-name", "True", "Success", `spec.githubAPI.host (\"%s\") is reachable and TLS verification succeeds`, *validFilledOutIDP.Spec.GitHubAPI.Host), + buildLogForUpdatingPhase("some-idp-name", "Error"), + }, + }, + { + name: "Organization Policy error - spec.allowAuthentication.organizations.policy must be 'AllGitHubUsers' when spec.allowAuthentication.organizations.allowed is empty", + secrets: []runtime.Object{goodSecret}, + githubIdentityProviders: []runtime.Object{ + func() runtime.Object { + badIDP := validFilledOutIDP.DeepCopy() + badIDP.Spec.AllowAuthentication.Organizations.Allowed = nil + return badIDP + }(), + }, + wantResultingUpstreams: []v1alpha1.GitHubIdentityProvider{ + { + ObjectMeta: validFilledOutIDP.ObjectMeta, + Spec: func() v1alpha1.GitHubIdentityProviderSpec { + badSpec := validFilledOutIDP.Spec.DeepCopy() + badSpec.AllowAuthentication.Organizations.Allowed = nil + return *badSpec + }(), + Status: v1alpha1.GitHubIdentityProviderStatus{ + Phase: v1alpha1.GitHubPhaseError, + Conditions: []metav1.Condition{ + buildClaimsValidatedTrue(t), + buildClientCredentialsSecretValidTrue(t, validFilledOutIDP.Spec.Client.SecretName), + buildGitHubConnectionValidTrue(t, *validFilledOutIDP.Spec.GitHubAPI.Host), + buildHostValidTrue(t, *validFilledOutIDP.Spec.GitHubAPI.Host), + buildOrganizationsPolicyValidFalse(t, "spec.allowAuthentication.organizations.policy must be 'AllGitHubUsers' when spec.allowAuthentication.organizations.allowed is empty"), + buildTLSConfigurationValidTrue(t), + }, + }, + }, + }, + wantLogs: []string{ + buildLogForUpdatingClientCredentialsSecretValid("some-idp-name", "True", "Success", fmt.Sprintf(`clientID and clientSecret have been read from spec.client.SecretName (\"%s\")`, validFilledOutIDP.Spec.Client.SecretName)), + buildLogForUpdatingClaimsValidTrue("some-idp-name"), + buildLogForUpdatingOrganizationPolicyValid("some-idp-name", "False", "Invalid", "spec.allowAuthentication.organizations.policy must be 'AllGitHubUsers' when spec.allowAuthentication.organizations.allowed is empty"), + buildLogForUpdatingHostValid("some-idp-name", "True", "Success", `spec.githubAPI.host (\"%s\") is valid`, *validFilledOutIDP.Spec.GitHubAPI.Host), + buildLogForUpdatingTLSConfigurationValid("some-idp-name", "True", "Success", "spec.githubAPI.tls.certificateAuthorityData is valid"), + buildLogForUpdatingGitHubConnectionValid("some-idp-name", "True", "Success", `spec.githubAPI.host (\"%s\") is reachable and TLS verification succeeds`, *validFilledOutIDP.Spec.GitHubAPI.Host), + buildLogForUpdatingPhase("some-idp-name", "Error"), + }, + }, + { + name: "Invalid Claims - missing spec.claims.username", + secrets: []runtime.Object{goodSecret}, + githubIdentityProviders: []runtime.Object{ + func() runtime.Object { + badIDP := validFilledOutIDP.DeepCopy() + badIDP.Spec.Claims.Username = nil + return badIDP + }(), + }, + wantResultingUpstreams: []v1alpha1.GitHubIdentityProvider{ + { + ObjectMeta: validFilledOutIDP.ObjectMeta, + Spec: func() v1alpha1.GitHubIdentityProviderSpec { + badSpec := validFilledOutIDP.Spec.DeepCopy() + badSpec.Claims.Username = nil + return *badSpec + }(), + Status: v1alpha1.GitHubIdentityProviderStatus{ + Phase: v1alpha1.GitHubPhaseError, + Conditions: []metav1.Condition{ + buildClaimsValidatedFalse(t, "spec.claims.username is required"), + buildClientCredentialsSecretValidTrue(t, validFilledOutIDP.Spec.Client.SecretName), + buildGitHubConnectionValidTrue(t, *validFilledOutIDP.Spec.GitHubAPI.Host), + buildHostValidTrue(t, *validFilledOutIDP.Spec.GitHubAPI.Host), + buildOrganizationsPolicyValidTrue(t, *validFilledOutIDP.Spec.AllowAuthentication.Organizations.Policy), + buildTLSConfigurationValidTrue(t), + }, + }, + }, + }, + wantLogs: []string{ + buildLogForUpdatingClientCredentialsSecretValid("some-idp-name", "True", "Success", fmt.Sprintf(`clientID and clientSecret have been read from spec.client.SecretName (\"%s\")`, validFilledOutIDP.Spec.Client.SecretName)), + buildLogForUpdatingClaimsValidFalse("some-idp-name", "spec.claims.username is required"), + buildLogForUpdatingOrganizationPolicyValid("some-idp-name", "True", "Success", fmt.Sprintf(`spec.allowAuthentication.organizations.policy (\"%s\") is valid`, string(*validFilledOutIDP.Spec.AllowAuthentication.Organizations.Policy))), + buildLogForUpdatingHostValid("some-idp-name", "True", "Success", `spec.githubAPI.host (\"%s\") is valid`, *validFilledOutIDP.Spec.GitHubAPI.Host), + buildLogForUpdatingTLSConfigurationValid("some-idp-name", "True", "Success", "spec.githubAPI.tls.certificateAuthorityData is valid"), + buildLogForUpdatingGitHubConnectionValid("some-idp-name", "True", "Success", `spec.githubAPI.host (\"%s\") is reachable and TLS verification succeeds`, *validFilledOutIDP.Spec.GitHubAPI.Host), + buildLogForUpdatingPhase("some-idp-name", "Error"), + }, + }, + { + name: "Invalid Claims - invalid spec.claims.username", + secrets: []runtime.Object{goodSecret}, + githubIdentityProviders: []runtime.Object{ + func() runtime.Object { + badIDP := validFilledOutIDP.DeepCopy() + badIDP.Spec.Claims.Username = ptr.To[v1alpha1.GitHubUsernameAttribute]("a") + return badIDP + }(), + }, + wantResultingUpstreams: []v1alpha1.GitHubIdentityProvider{ + { + ObjectMeta: validFilledOutIDP.ObjectMeta, + Spec: func() v1alpha1.GitHubIdentityProviderSpec { + badSpec := validFilledOutIDP.Spec.DeepCopy() + badSpec.Claims.Username = ptr.To[v1alpha1.GitHubUsernameAttribute]("a") + return *badSpec + }(), + Status: v1alpha1.GitHubIdentityProviderStatus{ + Phase: v1alpha1.GitHubPhaseError, + Conditions: []metav1.Condition{ + buildClaimsValidatedFalse(t, `spec.claims.username ("a") is not valid`), + buildClientCredentialsSecretValidTrue(t, validFilledOutIDP.Spec.Client.SecretName), + buildGitHubConnectionValidTrue(t, *validFilledOutIDP.Spec.GitHubAPI.Host), + buildHostValidTrue(t, *validFilledOutIDP.Spec.GitHubAPI.Host), + buildOrganizationsPolicyValidTrue(t, *validFilledOutIDP.Spec.AllowAuthentication.Organizations.Policy), + buildTLSConfigurationValidTrue(t), + }, + }, + }, + }, + wantLogs: []string{ + buildLogForUpdatingClientCredentialsSecretValid("some-idp-name", "True", "Success", fmt.Sprintf(`clientID and clientSecret have been read from spec.client.SecretName (\"%s\")`, validFilledOutIDP.Spec.Client.SecretName)), + buildLogForUpdatingClaimsValidFalse("some-idp-name", `spec.claims.username (\"a\") is not valid`), + buildLogForUpdatingOrganizationPolicyValid("some-idp-name", "True", "Success", fmt.Sprintf(`spec.allowAuthentication.organizations.policy (\"%s\") is valid`, string(*validFilledOutIDP.Spec.AllowAuthentication.Organizations.Policy))), + buildLogForUpdatingHostValid("some-idp-name", "True", "Success", `spec.githubAPI.host (\"%s\") is valid`, *validFilledOutIDP.Spec.GitHubAPI.Host), + buildLogForUpdatingTLSConfigurationValid("some-idp-name", "True", "Success", "spec.githubAPI.tls.certificateAuthorityData is valid"), + buildLogForUpdatingGitHubConnectionValid("some-idp-name", "True", "Success", `spec.githubAPI.host (\"%s\") is reachable and TLS verification succeeds`, *validFilledOutIDP.Spec.GitHubAPI.Host), + buildLogForUpdatingPhase("some-idp-name", "Error"), + }, + }, + { + name: "Invalid Claims - missing spec.claims.groups", + secrets: []runtime.Object{goodSecret}, + githubIdentityProviders: []runtime.Object{ + func() runtime.Object { + badIDP := validFilledOutIDP.DeepCopy() + badIDP.Spec.Claims.Groups = nil + return badIDP + }(), + }, + wantResultingUpstreams: []v1alpha1.GitHubIdentityProvider{ + { + ObjectMeta: validFilledOutIDP.ObjectMeta, + Spec: func() v1alpha1.GitHubIdentityProviderSpec { + badSpec := validFilledOutIDP.Spec.DeepCopy() + badSpec.Claims.Groups = nil + return *badSpec + }(), + Status: v1alpha1.GitHubIdentityProviderStatus{ + Phase: v1alpha1.GitHubPhaseError, + Conditions: []metav1.Condition{ + buildClaimsValidatedFalse(t, "spec.claims.groups is required"), + buildClientCredentialsSecretValidTrue(t, validFilledOutIDP.Spec.Client.SecretName), + buildGitHubConnectionValidTrue(t, *validFilledOutIDP.Spec.GitHubAPI.Host), + buildHostValidTrue(t, *validFilledOutIDP.Spec.GitHubAPI.Host), + buildOrganizationsPolicyValidTrue(t, *validFilledOutIDP.Spec.AllowAuthentication.Organizations.Policy), + buildTLSConfigurationValidTrue(t), + }, + }, + }, + }, + wantLogs: []string{ + buildLogForUpdatingClientCredentialsSecretValid("some-idp-name", "True", "Success", fmt.Sprintf(`clientID and clientSecret have been read from spec.client.SecretName (\"%s\")`, validFilledOutIDP.Spec.Client.SecretName)), + buildLogForUpdatingClaimsValidFalse("some-idp-name", "spec.claims.groups is required"), + buildLogForUpdatingOrganizationPolicyValid("some-idp-name", "True", "Success", fmt.Sprintf(`spec.allowAuthentication.organizations.policy (\"%s\") is valid`, string(*validFilledOutIDP.Spec.AllowAuthentication.Organizations.Policy))), + buildLogForUpdatingHostValid("some-idp-name", "True", "Success", `spec.githubAPI.host (\"%s\") is valid`, *validFilledOutIDP.Spec.GitHubAPI.Host), + buildLogForUpdatingTLSConfigurationValid("some-idp-name", "True", "Success", "spec.githubAPI.tls.certificateAuthorityData is valid"), + buildLogForUpdatingGitHubConnectionValid("some-idp-name", "True", "Success", `spec.githubAPI.host (\"%s\") is reachable and TLS verification succeeds`, *validFilledOutIDP.Spec.GitHubAPI.Host), + buildLogForUpdatingPhase("some-idp-name", "Error"), + }, + }, + { + name: "Invalid Claims - invalid spec.claims.groups", + secrets: []runtime.Object{goodSecret}, + githubIdentityProviders: []runtime.Object{ + func() runtime.Object { + badIDP := validFilledOutIDP.DeepCopy() + badIDP.Spec.Claims.Groups = ptr.To[v1alpha1.GitHubGroupNameAttribute]("b") + return badIDP + }(), + }, + wantResultingUpstreams: []v1alpha1.GitHubIdentityProvider{ + { + ObjectMeta: validFilledOutIDP.ObjectMeta, + Spec: func() v1alpha1.GitHubIdentityProviderSpec { + badSpec := validFilledOutIDP.Spec.DeepCopy() + badSpec.Claims.Groups = ptr.To[v1alpha1.GitHubGroupNameAttribute]("b") + return *badSpec + }(), + Status: v1alpha1.GitHubIdentityProviderStatus{ + Phase: v1alpha1.GitHubPhaseError, + Conditions: []metav1.Condition{ + buildClaimsValidatedFalse(t, `spec.claims.groups ("b") is not valid`), + buildClientCredentialsSecretValidTrue(t, validFilledOutIDP.Spec.Client.SecretName), + buildGitHubConnectionValidTrue(t, *validFilledOutIDP.Spec.GitHubAPI.Host), + buildHostValidTrue(t, *validFilledOutIDP.Spec.GitHubAPI.Host), + buildOrganizationsPolicyValidTrue(t, *validFilledOutIDP.Spec.AllowAuthentication.Organizations.Policy), + buildTLSConfigurationValidTrue(t), + }, + }, + }, + }, + wantLogs: []string{ + buildLogForUpdatingClientCredentialsSecretValid("some-idp-name", "True", "Success", fmt.Sprintf(`clientID and clientSecret have been read from spec.client.SecretName (\"%s\")`, validFilledOutIDP.Spec.Client.SecretName)), + buildLogForUpdatingClaimsValidFalse("some-idp-name", `spec.claims.groups (\"b\") is not valid`), + buildLogForUpdatingOrganizationPolicyValid("some-idp-name", "True", "Success", fmt.Sprintf(`spec.allowAuthentication.organizations.policy (\"%s\") is valid`, string(*validFilledOutIDP.Spec.AllowAuthentication.Organizations.Policy))), + buildLogForUpdatingHostValid("some-idp-name", "True", "Success", `spec.githubAPI.host (\"%s\") is valid`, *validFilledOutIDP.Spec.GitHubAPI.Host), + buildLogForUpdatingTLSConfigurationValid("some-idp-name", "True", "Success", "spec.githubAPI.tls.certificateAuthorityData is valid"), + buildLogForUpdatingGitHubConnectionValid("some-idp-name", "True", "Success", `spec.githubAPI.host (\"%s\") is reachable and TLS verification succeeds`, *validFilledOutIDP.Spec.GitHubAPI.Host), + buildLogForUpdatingPhase("some-idp-name", "Error"), + }, + }, + { + name: "Client Secret error - in different namespace", + secrets: []runtime.Object{ + func() runtime.Object { + badSecret := goodSecret.DeepCopy() + badSecret.Namespace = "other-namespace" + return badSecret + }(), + }, + githubIdentityProviders: []runtime.Object{validMinimalIDP}, + wantResultingUpstreams: []v1alpha1.GitHubIdentityProvider{ + { + ObjectMeta: validMinimalIDP.ObjectMeta, + Spec: validMinimalIDP.Spec, + Status: v1alpha1.GitHubIdentityProviderStatus{ + Phase: v1alpha1.GitHubPhaseError, + Conditions: []metav1.Condition{ + buildClaimsValidatedTrue(t), + buildClientCredentialsSecretValidFalse( + t, + fmt.Sprintf("secret %q not found", validMinimalIDP.Spec.Client.SecretName), + validMinimalIDP.Spec.Client.SecretName, + validMinimalIDP.Namespace, + upstreamwatchers.ReasonNotFound, + ), + buildGitHubConnectionValidTrue(t, *validMinimalIDP.Spec.GitHubAPI.Host), + buildHostValidTrue(t, *validMinimalIDP.Spec.GitHubAPI.Host), + buildOrganizationsPolicyValidTrue(t, *validMinimalIDP.Spec.AllowAuthentication.Organizations.Policy), + buildTLSConfigurationValidTrue(t), + }, + }, + }, + }, + wantLogs: []string{ + buildLogForUpdatingClientCredentialsSecretValid("minimal-idp-name", "False", "SecretNotFound", `secret \"some-secret-name\" not found: secret from spec.client.SecretName (\"some-secret-name\") must be found in namespace \"some-namespace\" with type \"secrets.pinniped.dev/github-client\" and keys \"clientID\" and \"clientSecret\"`), + buildLogForUpdatingClaimsValidTrue("minimal-idp-name"), + buildLogForUpdatingOrganizationPolicyValid("minimal-idp-name", "True", "Success", fmt.Sprintf(`spec.allowAuthentication.organizations.policy (\"%s\") is valid`, string(*validMinimalIDP.Spec.AllowAuthentication.Organizations.Policy))), + buildLogForUpdatingHostValid("minimal-idp-name", "True", "Success", `spec.githubAPI.host (\"%s\") is valid`, *validFilledOutIDP.Spec.GitHubAPI.Host), + buildLogForUpdatingTLSConfigurationValid("minimal-idp-name", "True", "Success", "spec.githubAPI.tls.certificateAuthorityData is valid"), + buildLogForUpdatingGitHubConnectionValid("minimal-idp-name", "True", "Success", `spec.githubAPI.host (\"%s\") is reachable and TLS verification succeeds`, *validFilledOutIDP.Spec.GitHubAPI.Host), + buildLogForUpdatingPhase("minimal-idp-name", "Error"), + }, + }, + { + name: "Client Secret error - wrong type", + secrets: []runtime.Object{ + func() runtime.Object { + badSecret := goodSecret.DeepCopy() + badSecret.Type = "other-type" + return badSecret + }(), + }, + githubIdentityProviders: []runtime.Object{validMinimalIDP}, + wantResultingUpstreams: []v1alpha1.GitHubIdentityProvider{ + { + ObjectMeta: validMinimalIDP.ObjectMeta, + Spec: validMinimalIDP.Spec, + Status: v1alpha1.GitHubIdentityProviderStatus{ + Phase: v1alpha1.GitHubPhaseError, + Conditions: []metav1.Condition{ + buildClaimsValidatedTrue(t), + buildClientCredentialsSecretValidFalse( + t, + `wrong secret type "other-type"`, + validMinimalIDP.Spec.Client.SecretName, + validMinimalIDP.Namespace, + upstreamwatchers.ReasonNotFound, + ), + buildGitHubConnectionValidTrue(t, *validMinimalIDP.Spec.GitHubAPI.Host), + buildHostValidTrue(t, *validMinimalIDP.Spec.GitHubAPI.Host), + buildOrganizationsPolicyValidTrue(t, *validMinimalIDP.Spec.AllowAuthentication.Organizations.Policy), + buildTLSConfigurationValidTrue(t), + }, + }, + }, + }, + wantLogs: []string{ + buildLogForUpdatingClientCredentialsSecretValid("minimal-idp-name", "False", "SecretNotFound", `wrong secret type \"other-type\": secret from spec.client.SecretName (\"some-secret-name\") must be found in namespace \"some-namespace\" with type \"secrets.pinniped.dev/github-client\" and keys \"clientID\" and \"clientSecret\"`), + buildLogForUpdatingClaimsValidTrue("minimal-idp-name"), + buildLogForUpdatingOrganizationPolicyValid("minimal-idp-name", "True", "Success", fmt.Sprintf(`spec.allowAuthentication.organizations.policy (\"%s\") is valid`, string(*validMinimalIDP.Spec.AllowAuthentication.Organizations.Policy))), + buildLogForUpdatingHostValid("minimal-idp-name", "True", "Success", `spec.githubAPI.host (\"%s\") is valid`, *validMinimalIDP.Spec.GitHubAPI.Host), + buildLogForUpdatingTLSConfigurationValid("minimal-idp-name", "True", "Success", "spec.githubAPI.tls.certificateAuthorityData is valid"), + buildLogForUpdatingGitHubConnectionValid("minimal-idp-name", "True", "Success", `spec.githubAPI.host (\"%s\") is reachable and TLS verification succeeds`, *validMinimalIDP.Spec.GitHubAPI.Host), + buildLogForUpdatingPhase("minimal-idp-name", "Error"), + }, + }, + { + name: "Client Secret error - missing clientId", + secrets: []runtime.Object{ + func() runtime.Object { + badSecret := goodSecret.DeepCopy() + delete(badSecret.Data, "clientID") + return badSecret + }(), + }, + githubIdentityProviders: []runtime.Object{validMinimalIDP}, + wantResultingUpstreams: []v1alpha1.GitHubIdentityProvider{ + { + ObjectMeta: validMinimalIDP.ObjectMeta, + Spec: validMinimalIDP.Spec, + Status: v1alpha1.GitHubIdentityProviderStatus{ + Phase: v1alpha1.GitHubPhaseError, + Conditions: []metav1.Condition{ + buildClaimsValidatedTrue(t), + buildClientCredentialsSecretValidFalse( + t, + `missing key "clientID"`, + validMinimalIDP.Spec.Client.SecretName, + validMinimalIDP.Namespace, + upstreamwatchers.ReasonNotFound, + ), + buildGitHubConnectionValidTrue(t, *validMinimalIDP.Spec.GitHubAPI.Host), + buildHostValidTrue(t, *validMinimalIDP.Spec.GitHubAPI.Host), + buildOrganizationsPolicyValidTrue(t, *validMinimalIDP.Spec.AllowAuthentication.Organizations.Policy), + buildTLSConfigurationValidTrue(t), + }, + }, + }, + }, + wantLogs: []string{ + buildLogForUpdatingClientCredentialsSecretValid("minimal-idp-name", "False", "SecretNotFound", `missing key \"clientID\": secret from spec.client.SecretName (\"some-secret-name\") must be found in namespace \"some-namespace\" with type \"secrets.pinniped.dev/github-client\" and keys \"clientID\" and \"clientSecret\"`), + buildLogForUpdatingClaimsValidTrue("minimal-idp-name"), + buildLogForUpdatingOrganizationPolicyValid("minimal-idp-name", "True", "Success", fmt.Sprintf(`spec.allowAuthentication.organizations.policy (\"%s\") is valid`, string(*validMinimalIDP.Spec.AllowAuthentication.Organizations.Policy))), + buildLogForUpdatingHostValid("minimal-idp-name", "True", "Success", `spec.githubAPI.host (\"%s\") is valid`, *validFilledOutIDP.Spec.GitHubAPI.Host), + buildLogForUpdatingTLSConfigurationValid("minimal-idp-name", "True", "Success", "spec.githubAPI.tls.certificateAuthorityData is valid"), + buildLogForUpdatingGitHubConnectionValid("minimal-idp-name", "True", "Success", `spec.githubAPI.host (\"%s\") is reachable and TLS verification succeeds`, *validFilledOutIDP.Spec.GitHubAPI.Host), + buildLogForUpdatingPhase("minimal-idp-name", "Error"), + }, + }, + { + name: "Client Secret error - missing clientSecret", + secrets: []runtime.Object{ + func() runtime.Object { + badSecret := goodSecret.DeepCopy() + delete(badSecret.Data, "clientSecret") + return badSecret + }(), + }, + githubIdentityProviders: []runtime.Object{validMinimalIDP}, + wantResultingUpstreams: []v1alpha1.GitHubIdentityProvider{ + { + ObjectMeta: validMinimalIDP.ObjectMeta, + Spec: validMinimalIDP.Spec, + Status: v1alpha1.GitHubIdentityProviderStatus{ + Phase: v1alpha1.GitHubPhaseError, + Conditions: []metav1.Condition{ + buildClaimsValidatedTrue(t), + buildClientCredentialsSecretValidFalse( + t, + `missing key "clientSecret"`, + validMinimalIDP.Spec.Client.SecretName, + validMinimalIDP.Namespace, + upstreamwatchers.ReasonNotFound, + ), + buildGitHubConnectionValidTrue(t, *validMinimalIDP.Spec.GitHubAPI.Host), + buildHostValidTrue(t, *validMinimalIDP.Spec.GitHubAPI.Host), + buildOrganizationsPolicyValidTrue(t, *validMinimalIDP.Spec.AllowAuthentication.Organizations.Policy), + buildTLSConfigurationValidTrue(t), + }, + }, + }, + }, + wantLogs: []string{ + buildLogForUpdatingClientCredentialsSecretValid("minimal-idp-name", "False", "SecretNotFound", `missing key \"clientSecret\": secret from spec.client.SecretName (\"some-secret-name\") must be found in namespace \"some-namespace\" with type \"secrets.pinniped.dev/github-client\" and keys \"clientID\" and \"clientSecret\"`), + buildLogForUpdatingClaimsValidTrue("minimal-idp-name"), + buildLogForUpdatingOrganizationPolicyValid("minimal-idp-name", "True", "Success", fmt.Sprintf(`spec.allowAuthentication.organizations.policy (\"%s\") is valid`, string(*validMinimalIDP.Spec.AllowAuthentication.Organizations.Policy))), + buildLogForUpdatingHostValid("minimal-idp-name", "True", "Success", `spec.githubAPI.host (\"%s\") is valid`, *validMinimalIDP.Spec.GitHubAPI.Host), + buildLogForUpdatingTLSConfigurationValid("minimal-idp-name", "True", "Success", "spec.githubAPI.tls.certificateAuthorityData is valid"), + buildLogForUpdatingGitHubConnectionValid("minimal-idp-name", "True", "Success", `spec.githubAPI.host (\"%s\") is reachable and TLS verification succeeds`, *validMinimalIDP.Spec.GitHubAPI.Host), + buildLogForUpdatingPhase("minimal-idp-name", "Error"), + }, + }, + { + name: "Client Secret error - additional data", + secrets: []runtime.Object{ + func() runtime.Object { + badSecret := goodSecret.DeepCopy() + badSecret.Data["foo"] = []byte("bar") + return badSecret + }(), + }, + githubIdentityProviders: []runtime.Object{validMinimalIDP}, + wantResultingUpstreams: []v1alpha1.GitHubIdentityProvider{ + { + ObjectMeta: validMinimalIDP.ObjectMeta, + Spec: validMinimalIDP.Spec, + Status: v1alpha1.GitHubIdentityProviderStatus{ + Phase: v1alpha1.GitHubPhaseError, + Conditions: []metav1.Condition{ + buildClaimsValidatedTrue(t), + buildClientCredentialsSecretValidFalse( + t, + "extra keys found", + validMinimalIDP.Spec.Client.SecretName, + validMinimalIDP.Namespace, + upstreamwatchers.ReasonNotFound, + ), + buildGitHubConnectionValidTrue(t, *validMinimalIDP.Spec.GitHubAPI.Host), + buildHostValidTrue(t, *validMinimalIDP.Spec.GitHubAPI.Host), + buildOrganizationsPolicyValidTrue(t, *validMinimalIDP.Spec.AllowAuthentication.Organizations.Policy), + buildTLSConfigurationValidTrue(t), + }, + }, + }, + }, + wantLogs: []string{ + buildLogForUpdatingClientCredentialsSecretValid("minimal-idp-name", "False", "SecretNotFound", `extra keys found: secret from spec.client.SecretName (\"some-secret-name\") must be found in namespace \"some-namespace\" with type \"secrets.pinniped.dev/github-client\" and keys \"clientID\" and \"clientSecret\"`), + buildLogForUpdatingClaimsValidTrue("minimal-idp-name"), + buildLogForUpdatingOrganizationPolicyValid("minimal-idp-name", "True", "Success", fmt.Sprintf(`spec.allowAuthentication.organizations.policy (\"%s\") is valid`, string(*validMinimalIDP.Spec.AllowAuthentication.Organizations.Policy))), + buildLogForUpdatingHostValid("minimal-idp-name", "True", "Success", `spec.githubAPI.host (\"%s\") is valid`, *validFilledOutIDP.Spec.GitHubAPI.Host), + buildLogForUpdatingTLSConfigurationValid("minimal-idp-name", "True", "Success", "spec.githubAPI.tls.certificateAuthorityData is valid"), + buildLogForUpdatingGitHubConnectionValid("minimal-idp-name", "True", "Success", `spec.githubAPI.host (\"%s\") is reachable and TLS verification succeeds`, *validFilledOutIDP.Spec.GitHubAPI.Host), + buildLogForUpdatingPhase("minimal-idp-name", "Error"), + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + fakeSupervisorClient := supervisorfake.NewSimpleClientset(tt.githubIdentityProviders...) + supervisorInformers := pinnipedinformers.NewSharedInformerFactory(fakeSupervisorClient, 0) + + fakeKubeClient := kubernetesfake.NewSimpleClientset(tt.secrets...) + kubeInformers := k8sinformers.NewSharedInformerFactoryWithOptions(fakeKubeClient, 0) + + cache := dynamicupstreamprovider.NewDynamicUpstreamIDPProvider() + cache.SetGitHubIdentityProviders([]upstreamprovider.UpstreamGithubIdentityProviderI{ + upstreamgithub.New( + upstreamgithub.ProviderConfig{Name: "initial-entry-to-remove"}, + ), + }) + + var log bytes.Buffer + logger := plog.TestLogger(t, &log) + + gitHubIdentityProviderInformer := supervisorInformers.IDP().V1alpha1().GitHubIdentityProviders() + + dialer := tt.mockDialer + if dialer == nil { + dialer = tls.Dial + } + + controller := New( + namespace, + cache, + fakeSupervisorClient, + gitHubIdentityProviderInformer, + kubeInformers.Core().V1().Secrets(), + logger, + controllerlib.WithInformer, + frozenClockForLastTransitionTime, + dialer, + ) + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + supervisorInformers.Start(ctx.Done()) + kubeInformers.Start(ctx.Done()) + controllerlib.TestRunSynchronously(t, controller) + + syncCtx := controllerlib.Context{Context: ctx, Key: controllerlib.Key{}} + + if err := controllerlib.TestSync(t, controller, syncCtx); len(tt.wantErr) > 0 { + require.ErrorContains(t, err, controllerlib.ErrSyntheticRequeue.Error()) + require.ErrorContains(t, err, tt.wantErr) + } else { + require.NoError(t, err) + } + + // Verify what's in the cache + actualIDPList := cache.GetGitHubIdentityProviders() + require.Equal(t, len(tt.wantResultingCache), len(actualIDPList)) + for i := range len(tt.wantResultingCache) { + // Do not expect any particular order in the cache + var actualProvider *upstreamgithub.Provider + for _, possibleIDP := range actualIDPList { + if possibleIDP.GetResourceName() == tt.wantResultingCache[i].Name { + var ok bool + actualProvider, ok = possibleIDP.(*upstreamgithub.Provider) + require.True(t, ok) + break + } + } + + require.Equal(t, tt.wantResultingCache[i].Name, actualProvider.GetResourceName()) + require.Equal(t, tt.wantResultingCache[i].ResourceUID, actualProvider.GetResourceUID()) + require.Equal(t, tt.wantResultingCache[i].OAuth2Config.ClientID, actualProvider.GetClientID()) + require.Equal(t, tt.wantResultingCache[i].GroupNameAttribute, actualProvider.GetGroupNameAttribute()) + require.Equal(t, tt.wantResultingCache[i].UsernameAttribute, actualProvider.GetUsernameAttribute()) + require.Equal(t, tt.wantResultingCache[i].AllowedOrganizations, actualProvider.GetAllowedOrganizations()) + + require.GreaterOrEqual(t, len(tt.githubIdentityProviders), i+1, "there must be at least as many input identity providers as items in the cache") + githubIDP, ok := tt.githubIdentityProviders[i].(*v1alpha1.GitHubIdentityProvider) + require.True(t, ok) + certPool, _, err := pinnipedcontroller.BuildCertPoolIDP(githubIDP.Spec.GitHubAPI.TLS) + require.NoError(t, err) + + compareTLSClientConfigWithinHttpClients(t, phttp.Default(certPool), actualProvider.GetConfig().HttpClient) + require.Equal(t, tt.wantResultingCache[i].OAuth2Config, actualProvider.GetConfig().OAuth2Config) + require.Contains(t, tt.wantResultingCache[i].APIBaseURL, actualProvider.GetConfig().APIBaseURL) + } + + // Verify the status conditions as reported in Kubernetes + allGitHubIDPs, err := fakeSupervisorClient.IDPV1alpha1().GitHubIdentityProviders(namespace).List(ctx, metav1.ListOptions{}) + require.NoError(t, err) + + require.Equal(t, len(tt.wantResultingUpstreams), len(allGitHubIDPs.Items)) + for i := range len(tt.wantResultingUpstreams) { + require.Len(t, tt.wantResultingUpstreams[i].Status.Conditions, countExpectedConditions) + + // Do not expect any particular order in the K8s objects + var actualIDP *v1alpha1.GitHubIdentityProvider + for _, possibleMatch := range allGitHubIDPs.Items { + if possibleMatch.GetName() == tt.wantResultingUpstreams[i].Name { + actualIDP = ptr.To(possibleMatch) + break + } + } + + require.NotNil(t, actualIDP, "must find IDP with name %s", tt.wantResultingUpstreams[i].Name) + require.Len(t, actualIDP.Status.Conditions, countExpectedConditions) + require.Equal(t, tt.wantResultingUpstreams[i], *actualIDP) + } + + expectedLogs := "" + if len(tt.wantLogs) > 0 { + expectedLogs = strings.Join(tt.wantLogs, "\n") + "\n" + } + require.Equal(t, expectedLogs, log.String()) + + // This needs to happen after the expected condition LastTransitionTime has been updated. + wantActions := make([]coretesting.Action, 3+len(tt.wantResultingUpstreams)) + wantActions[0] = coretesting.NewListAction(githubIDPGVR, githubIDPKind, "", metav1.ListOptions{}) + wantActions[1] = coretesting.NewWatchAction(githubIDPGVR, "", metav1.ListOptions{}) + for i, want := range tt.wantResultingUpstreams { + wantActions[2+i] = coretesting.NewUpdateSubresourceAction(githubIDPGVR, "status", want.Namespace, ptr.To(want)) + } + // This List action is coming from the test code when it pulls the GitHubIdentityProviders from K8s + wantActions[len(wantActions)-1] = coretesting.NewListAction(githubIDPGVR, githubIDPKind, namespace, metav1.ListOptions{}) + require.Equal(t, wantActions, fakeSupervisorClient.Actions()) + }) + } +} + +func TestController_OnlyWantActions(t *testing.T) { + require.Equal(t, 6, countExpectedConditions) + + goodServer, goodServerCA := tlsserver.TestServerIPv4(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + }), tlsserver.RecordTLSHello) + goodServerDomain, _ := strings.CutPrefix(goodServer.URL, "https://") + goodServerCAB64 := base64.StdEncoding.EncodeToString(goodServerCA) + + oneHourAgo := metav1.Time{Time: time.Now().Add(-1 * time.Hour)} + namespace := "existing-conditions-namespace" + + wantFrozenTime := time.Date(1122, time.September, 33, 4, 55, 56, 778899, time.Local) + frozenClockForLastTransitionTime := clocktesting.NewFakeClock(wantFrozenTime) + wantLastTransitionTime := metav1.Time{Time: wantFrozenTime} + + goodSecret := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "some-secret-name", + Namespace: namespace, + }, + Type: "secrets.pinniped.dev/github-client", + Data: map[string][]byte{ + "clientID": []byte("some-client-id"), + "clientSecret": []byte("some-client-secret"), + }, + } + + validMinimalIDP := &v1alpha1.GitHubIdentityProvider{ + ObjectMeta: metav1.ObjectMeta{ + Name: "minimal-idp-name", + Namespace: namespace, + UID: types.UID("minimal-uid"), + Generation: 1234, + }, + Spec: v1alpha1.GitHubIdentityProviderSpec{ + GitHubAPI: v1alpha1.GitHubAPIConfig{ + Host: ptr.To(goodServerDomain), + TLS: &v1alpha1.TLSSpec{ + CertificateAuthorityData: goodServerCAB64, + }, + }, + // These claims are optional when using the actual Kubernetes CRD. + // However, they are required here because CRD defaulting/validation does not occur during testing. + Claims: v1alpha1.GitHubClaims{ + Username: ptr.To(v1alpha1.GitHubUsernameLogin), + Groups: ptr.To(v1alpha1.GitHubUseTeamSlugForGroupName), + }, + Client: v1alpha1.GitHubClientSpec{ + SecretName: goodSecret.Name, + }, + AllowAuthentication: v1alpha1.GitHubAllowAuthenticationSpec{ + Organizations: v1alpha1.GitHubOrganizationsSpec{ + Policy: ptr.To(v1alpha1.GitHubAllowedAuthOrganizationsPolicyAllGitHubUsers), + }, + }, + }, + } + + alreadyInvalidExistingIDP := &v1alpha1.GitHubIdentityProvider{ + ObjectMeta: metav1.ObjectMeta{ + Name: "already-existing-invalid-idp-name", + Namespace: namespace, + UID: types.UID("some-resource-uid"), + Generation: 333, + }, + Spec: v1alpha1.GitHubIdentityProviderSpec{ + GitHubAPI: v1alpha1.GitHubAPIConfig{ + Host: ptr.To(goodServerDomain), + TLS: &v1alpha1.TLSSpec{ + CertificateAuthorityData: goodServerCAB64, + }, + }, + AllowAuthentication: v1alpha1.GitHubAllowAuthenticationSpec{ + Organizations: v1alpha1.GitHubOrganizationsSpec{ + Policy: ptr.To(v1alpha1.GitHubAllowedAuthOrganizationsPolicyAllGitHubUsers), + }, + }, + Claims: v1alpha1.GitHubClaims{ + Groups: ptr.To(v1alpha1.GitHubUseTeamSlugForGroupName), + }, + Client: v1alpha1.GitHubClientSpec{ + SecretName: "unknown-secret", + }, + }, + Status: v1alpha1.GitHubIdentityProviderStatus{ + Phase: v1alpha1.GitHubPhaseError, + Conditions: []metav1.Condition{ + { + Type: ClaimsValid, + Status: metav1.ConditionFalse, + ObservedGeneration: 333, + LastTransitionTime: oneHourAgo, + Reason: "Invalid", + Message: "spec.claims.username is required", + }, + { + Type: ClientCredentialsSecretValid, + Status: metav1.ConditionFalse, + ObservedGeneration: 333, + LastTransitionTime: oneHourAgo, + Reason: "SecretNotFound", + Message: fmt.Sprintf(`secret "unknown-secret" not found: secret from spec.client.SecretName ("unknown-secret") must be found in namespace %q with type "secrets.pinniped.dev/github-client" and keys "clientID" and "clientSecret"`, namespace), + }, + { + Type: GitHubConnectionValid, + Status: metav1.ConditionTrue, + ObservedGeneration: 333, + LastTransitionTime: oneHourAgo, + Reason: upstreamwatchers.ReasonSuccess, + Message: fmt.Sprintf("spec.githubAPI.host (%q) is reachable and TLS verification succeeds", goodServerDomain), + }, + { + Type: HostValid, + Status: metav1.ConditionTrue, + ObservedGeneration: 333, + LastTransitionTime: oneHourAgo, + Reason: upstreamwatchers.ReasonSuccess, + Message: fmt.Sprintf("spec.githubAPI.host (%q) is valid", goodServerDomain), + }, + { + Type: OrganizationsPolicyValid, + Status: metav1.ConditionTrue, + ObservedGeneration: 333, + LastTransitionTime: oneHourAgo, + Reason: upstreamwatchers.ReasonSuccess, + Message: `spec.allowAuthentication.organizations.policy ("AllGitHubUsers") is valid`, + }, + { + Type: TLSConfigurationValid, + Status: metav1.ConditionTrue, + ObservedGeneration: 333, + LastTransitionTime: oneHourAgo, + Reason: upstreamwatchers.ReasonSuccess, + Message: "spec.githubAPI.tls.certificateAuthorityData is valid", + }, + }, + }, + } + + tests := []struct { + name string + secrets []runtime.Object + githubIdentityProviders []runtime.Object + addSupervisorReactors func(*supervisorfake.Clientset) + wantErr string + wantActions []coretesting.Action + }{ + { + name: "no GitHubIdentityProviders", + wantActions: make([]coretesting.Action, 0), + }, + { + name: "already existing idp with appropriate conditions does not issue actions", + githubIdentityProviders: []runtime.Object{ + alreadyInvalidExistingIDP, + }, + wantActions: make([]coretesting.Action, 0), + }, + { + name: "already existing idp with stale conditions will issue an update action", + githubIdentityProviders: []runtime.Object{ + func() runtime.Object { + otherIDP := alreadyInvalidExistingIDP.DeepCopy() + otherIDP.Generation = 400 + otherIDP.Status.Phase = v1alpha1.GitHubPhaseReady + otherIDP.Status.Conditions[0].Status = metav1.ConditionTrue + otherIDP.Status.Conditions[0].Message = "some other message indicating that things are good" + return otherIDP + }(), + }, + wantActions: []coretesting.Action{ + func() coretesting.Action { + idp := alreadyInvalidExistingIDP.DeepCopy() + idp.Generation = 400 + for i := range idp.Status.Conditions { + idp.Status.Conditions[i].ObservedGeneration = 400 + } + idp.Status.Conditions[0].LastTransitionTime = wantLastTransitionTime + wantAction := coretesting.NewUpdateSubresourceAction(githubIDPGVR, "status", namespace, idp) + return wantAction + }(), + }, + }, + { + name: "K8s client error - cannot update githubidentityproviders", + secrets: []runtime.Object{goodSecret}, + githubIdentityProviders: []runtime.Object{validMinimalIDP}, + wantErr: "error from reactor - unable to update", + addSupervisorReactors: func(fake *supervisorfake.Clientset) { + fake.PrependReactor("update", "githubidentityproviders", func(_ coretesting.Action) (bool, runtime.Object, error) { + return true, nil, fmt.Errorf("error from reactor - unable to update") + }) + }, + wantActions: []coretesting.Action{ + coretesting.NewUpdateSubresourceAction(githubIDPGVR, "status", namespace, func() runtime.Object { + idpWithConditions := validMinimalIDP.DeepCopy() + idpWithConditions.Status = v1alpha1.GitHubIdentityProviderStatus{ + Phase: v1alpha1.GitHubPhaseReady, + Conditions: []metav1.Condition{ + { + Type: ClaimsValid, + Status: metav1.ConditionTrue, + ObservedGeneration: 1234, + LastTransitionTime: wantLastTransitionTime, + Reason: upstreamwatchers.ReasonSuccess, + Message: "spec.claims are valid", + }, + { + Type: ClientCredentialsSecretValid, + Status: metav1.ConditionTrue, + ObservedGeneration: 1234, + LastTransitionTime: wantLastTransitionTime, + Reason: upstreamwatchers.ReasonSuccess, + Message: `clientID and clientSecret have been read from spec.client.SecretName ("some-secret-name")`, + }, + { + Type: GitHubConnectionValid, + Status: metav1.ConditionTrue, + ObservedGeneration: 1234, + LastTransitionTime: wantLastTransitionTime, + Reason: upstreamwatchers.ReasonSuccess, + Message: fmt.Sprintf("spec.githubAPI.host (%q) is reachable and TLS verification succeeds", goodServerDomain), + }, + { + Type: HostValid, + Status: metav1.ConditionTrue, + ObservedGeneration: 1234, + LastTransitionTime: wantLastTransitionTime, + Reason: upstreamwatchers.ReasonSuccess, + Message: fmt.Sprintf("spec.githubAPI.host (%q) is valid", goodServerDomain), + }, + { + Type: OrganizationsPolicyValid, + Status: metav1.ConditionTrue, + ObservedGeneration: 1234, + LastTransitionTime: wantLastTransitionTime, + Reason: upstreamwatchers.ReasonSuccess, + Message: `spec.allowAuthentication.organizations.policy ("AllGitHubUsers") is valid`, + }, + { + Type: TLSConfigurationValid, + Status: metav1.ConditionTrue, + ObservedGeneration: 1234, + LastTransitionTime: wantLastTransitionTime, + Reason: upstreamwatchers.ReasonSuccess, + Message: "spec.githubAPI.tls.certificateAuthorityData is valid", + }, + }, + } + return idpWithConditions + }()), + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + fakeSupervisorClient := supervisorfake.NewSimpleClientset(tt.githubIdentityProviders...) + supervisorInformers := pinnipedinformers.NewSharedInformerFactory(supervisorfake.NewSimpleClientset(tt.githubIdentityProviders...), 0) + + if tt.addSupervisorReactors != nil { + tt.addSupervisorReactors(fakeSupervisorClient) + } + + kubeInformers := k8sinformers.NewSharedInformerFactoryWithOptions(kubernetesfake.NewSimpleClientset(tt.secrets...), 0) + + var log bytes.Buffer + logger := plog.TestLogger(t, &log) + + controller := New( + namespace, + dynamicupstreamprovider.NewDynamicUpstreamIDPProvider(), + fakeSupervisorClient, + supervisorInformers.IDP().V1alpha1().GitHubIdentityProviders(), + kubeInformers.Core().V1().Secrets(), + logger, + controllerlib.WithInformer, + frozenClockForLastTransitionTime, + tls.Dial, + ) + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + supervisorInformers.Start(ctx.Done()) + kubeInformers.Start(ctx.Done()) + controllerlib.TestRunSynchronously(t, controller) + + syncCtx := controllerlib.Context{Context: ctx, Key: controllerlib.Key{}} + + if err := controllerlib.TestSync(t, controller, syncCtx); len(tt.wantErr) > 0 { + require.ErrorContains(t, err, controllerlib.ErrSyntheticRequeue.Error()) + require.ErrorContains(t, err, tt.wantErr) + } else { + require.NoError(t, err) + } + require.Equal(t, tt.wantActions, fakeSupervisorClient.Actions()) + }) + } +} + +func compareTLSClientConfigWithinHttpClients(t *testing.T, expected *http.Client, actual *http.Client) { + t.Helper() + + require.NotEmpty(t, expected) + require.NotEmpty(t, actual) + + require.Equal(t, expected.Timeout, actual.Timeout) + + expectedConfig, err := utilnet.TLSClientConfig(expected.Transport) + require.NoError(t, err) + + actualConfig, err := utilnet.TLSClientConfig(actual.Transport) + require.NoError(t, err) + + require.True(t, actualConfig.RootCAs.Equal(expectedConfig.RootCAs)) + actualConfig.RootCAs = expectedConfig.RootCAs + require.Equal(t, expectedConfig, actualConfig) +} + +func TestGitHubUpstreamWatcherControllerFilterSecret(t *testing.T) { + namespace := "some-namespace" + goodSecret := &corev1.Secret{ + Type: "secrets.pinniped.dev/github-client", + ObjectMeta: metav1.ObjectMeta{ + Name: "some-name", + Namespace: namespace, + }, + } + + tests := []struct { + name string + secret metav1.Object + wantAdd bool + wantUpdate bool + wantDelete bool + }{ + { + name: "a secret of the right type", + secret: goodSecret, + wantAdd: true, + wantUpdate: true, + wantDelete: true, + }, + { + name: "a secret of the right type, but in the wrong namespace", + secret: func() *corev1.Secret { + otherSecret := goodSecret.DeepCopy() + otherSecret.Namespace = "other-namespace" + return otherSecret + }(), + }, + { + name: "a secret of the wrong type", + secret: func() *corev1.Secret { + otherSecret := goodSecret.DeepCopy() + otherSecret.Type = "other-type" + return otherSecret + }(), + }, + { + name: "resource of wrong data type", + secret: &corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{Name: "some-name", Namespace: "some-namespace"}, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + kubeInformers := k8sinformers.NewSharedInformerFactoryWithOptions(kubernetesfake.NewSimpleClientset(), 0) + + var log bytes.Buffer + logger := plog.TestLogger(t, &log) + + secretInformer := kubeInformers.Core().V1().Secrets() + observableInformers := testutil.NewObservableWithInformerOption() + + _ = New( + namespace, + dynamicupstreamprovider.NewDynamicUpstreamIDPProvider(), + supervisorfake.NewSimpleClientset(), + pinnipedinformers.NewSharedInformerFactory(supervisorfake.NewSimpleClientset(), 0).IDP().V1alpha1().GitHubIdentityProviders(), + secretInformer, + logger, + observableInformers.WithInformer, + clock.RealClock{}, + tls.Dial, + ) + + unrelated := &corev1.Secret{} + filter := observableInformers.GetFilterForInformer(secretInformer) + require.Equal(t, tt.wantAdd, filter.Add(tt.secret)) + require.Equal(t, tt.wantUpdate, filter.Update(unrelated, tt.secret)) + require.Equal(t, tt.wantUpdate, filter.Update(tt.secret, unrelated)) + require.Equal(t, tt.wantDelete, filter.Delete(tt.secret)) + }) + } +} + +func TestGitHubUpstreamWatcherControllerFilterGitHubIDP(t *testing.T) { + namespace := "some-namespace" + goodIDP := &v1alpha1.GitHubIdentityProvider{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: namespace, + }, + } + + tests := []struct { + name string + idp metav1.Object + wantAdd bool + wantUpdate bool + wantDelete bool + }{ + { + name: "an IDP in the right namespace", + idp: goodIDP, + wantAdd: true, + wantUpdate: true, + wantDelete: true, + }, + { + name: "IDP in the wrong namespace", + idp: func() metav1.Object { + badIDP := goodIDP.DeepCopy() + badIDP.Namespace = "other-namespace" + return badIDP + }(), + }, + { + name: "resource of wrong data type", + idp: &corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{Name: "some-name"}, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + var log bytes.Buffer + logger := plog.TestLogger(t, &log) + + gitHubIdentityProviderInformer := pinnipedinformers.NewSharedInformerFactory(supervisorfake.NewSimpleClientset(), 0).IDP().V1alpha1().GitHubIdentityProviders() + observableInformers := testutil.NewObservableWithInformerOption() + + _ = New( + namespace, + dynamicupstreamprovider.NewDynamicUpstreamIDPProvider(), + supervisorfake.NewSimpleClientset(), + gitHubIdentityProviderInformer, + k8sinformers.NewSharedInformerFactoryWithOptions(kubernetesfake.NewSimpleClientset(), 0).Core().V1().Secrets(), + logger, + observableInformers.WithInformer, + clock.RealClock{}, + tls.Dial, + ) + + unrelated := &v1alpha1.GitHubIdentityProvider{} + filter := observableInformers.GetFilterForInformer(gitHubIdentityProviderInformer) + require.Equal(t, tt.wantAdd, filter.Add(tt.idp)) + require.Equal(t, tt.wantUpdate, filter.Update(unrelated, tt.idp)) + require.Equal(t, tt.wantUpdate, filter.Update(tt.idp, unrelated)) + require.Equal(t, tt.wantDelete, filter.Delete(tt.idp)) + }) + } +} diff --git a/internal/controller/supervisorconfig/ldapupstreamwatcher/ldap_upstream_watcher.go b/internal/controller/supervisorconfig/ldapupstreamwatcher/ldap_upstream_watcher.go index 9d7ff8f72..820de4b48 100644 --- a/internal/controller/supervisorconfig/ldapupstreamwatcher/ldap_upstream_watcher.go +++ b/internal/controller/supervisorconfig/ldapupstreamwatcher/ldap_upstream_watcher.go @@ -260,7 +260,7 @@ func (c *ldapWatcherController) updateStatus(ctx context.Context, upstream *idpv log := plog.WithValues("namespace", upstream.Namespace, "name", upstream.Name) updated := upstream.DeepCopy() - hadErrorCondition := conditionsutil.MergeIDPConditions(conditions, upstream.Generation, &updated.Status.Conditions, log) + hadErrorCondition := conditionsutil.MergeConditions(conditions, upstream.Generation, &updated.Status.Conditions, log, metav1.Now()) updated.Status.Phase = idpv1alpha1.LDAPPhaseReady if hadErrorCondition { diff --git a/internal/controller/supervisorconfig/oidcclientwatcher/oidc_client_watcher.go b/internal/controller/supervisorconfig/oidcclientwatcher/oidc_client_watcher.go index 01f630171..4086f3532 100644 --- a/internal/controller/supervisorconfig/oidcclientwatcher/oidc_client_watcher.go +++ b/internal/controller/supervisorconfig/oidcclientwatcher/oidc_client_watcher.go @@ -133,7 +133,7 @@ func (c *oidcClientWatcherController) updateStatus( ) error { updated := upstream.DeepCopy() - hadErrorCondition := conditionsutil.MergeConfigConditions(conditions, + hadErrorCondition := conditionsutil.MergeConditions(conditions, upstream.Generation, &updated.Status.Conditions, plog.New(), metav1.Now()) updated.Status.Phase = supervisorconfigv1alpha1.OIDCClientPhaseReady diff --git a/internal/controller/supervisorconfig/oidcupstreamwatcher/oidc_upstream_watcher.go b/internal/controller/supervisorconfig/oidcupstreamwatcher/oidc_upstream_watcher.go index 6a624bf58..f659e55d5 100644 --- a/internal/controller/supervisorconfig/oidcupstreamwatcher/oidc_upstream_watcher.go +++ b/internal/controller/supervisorconfig/oidcupstreamwatcher/oidc_upstream_watcher.go @@ -54,7 +54,7 @@ const ( oidcValidatorCacheTTL = 15 * time.Minute // Constants related to conditions. - typeClientCredentialsValid = "ClientCredentialsValid" //nolint:gosec // this is not a credential + typeClientCredentialsSecretValid = "ClientCredentialsSecretValid" //nolint:gosec // this is not a credential typeAdditionalAuthorizeParametersValid = "AdditionalAuthorizeParametersValid" typeOIDCDiscoverySucceeded = "OIDCDiscoverySucceeded" @@ -260,7 +260,7 @@ func (c *oidcWatcherController) validateUpstream(ctx controllerlib.Context, upst return nil } -// validateSecret validates the .spec.client.secretName field and returns the appropriate ClientCredentialsValid condition. +// validateSecret validates the .spec.client.secretName field and returns the appropriate ClientCredentialsSecretValid condition. func (c *oidcWatcherController) validateSecret(upstream *idpv1alpha1.OIDCIdentityProvider, result *upstreamoidc.ProviderConfig) *metav1.Condition { secretName := upstream.Spec.Client.SecretName @@ -268,7 +268,7 @@ func (c *oidcWatcherController) validateSecret(upstream *idpv1alpha1.OIDCIdentit secret, err := c.secretInformer.Lister().Secrets(upstream.Namespace).Get(secretName) if err != nil { return &metav1.Condition{ - Type: typeClientCredentialsValid, + Type: typeClientCredentialsSecretValid, Status: metav1.ConditionFalse, Reason: upstreamwatchers.ReasonNotFound, Message: err.Error(), @@ -278,7 +278,7 @@ func (c *oidcWatcherController) validateSecret(upstream *idpv1alpha1.OIDCIdentit // Validate the secret .type field. if secret.Type != oidcClientSecretType { return &metav1.Condition{ - Type: typeClientCredentialsValid, + Type: typeClientCredentialsSecretValid, Status: metav1.ConditionFalse, Reason: upstreamwatchers.ReasonWrongType, Message: fmt.Sprintf("referenced Secret %q has wrong type %q (should be %q)", secretName, secret.Type, oidcClientSecretType), @@ -290,7 +290,7 @@ func (c *oidcWatcherController) validateSecret(upstream *idpv1alpha1.OIDCIdentit clientSecret := secret.Data[clientSecretDataKey] if len(clientID) == 0 || len(clientSecret) == 0 { return &metav1.Condition{ - Type: typeClientCredentialsValid, + Type: typeClientCredentialsSecretValid, Status: metav1.ConditionFalse, Reason: upstreamwatchers.ReasonMissingKeys, Message: fmt.Sprintf("referenced Secret %q is missing required keys %q", secretName, []string{clientIDDataKey, clientSecretDataKey}), @@ -301,7 +301,7 @@ func (c *oidcWatcherController) validateSecret(upstream *idpv1alpha1.OIDCIdentit result.Config.ClientID = string(clientID) result.Config.ClientSecret = string(clientSecret) return &metav1.Condition{ - Type: typeClientCredentialsValid, + Type: typeClientCredentialsSecretValid, Status: metav1.ConditionTrue, Reason: upstreamwatchers.ReasonSuccess, Message: "loaded client credentials", @@ -412,7 +412,7 @@ func (c *oidcWatcherController) updateStatus(ctx context.Context, upstream *idpv log := c.log.WithValues("namespace", upstream.Namespace, "name", upstream.Name) updated := upstream.DeepCopy() - hadErrorCondition := conditionsutil.MergeIDPConditions(conditions, upstream.Generation, &updated.Status.Conditions, log) + hadErrorCondition := conditionsutil.MergeConditions(conditions, upstream.Generation, &updated.Status.Conditions, log, metav1.Now()) updated.Status.Phase = idpv1alpha1.PhaseReady if hadErrorCondition { diff --git a/internal/controller/supervisorconfig/oidcupstreamwatcher/oidc_upstream_watcher_test.go b/internal/controller/supervisorconfig/oidcupstreamwatcher/oidc_upstream_watcher_test.go index ab813242d..53f9356d4 100644 --- a/internal/controller/supervisorconfig/oidcupstreamwatcher/oidc_upstream_watcher_test.go +++ b/internal/controller/supervisorconfig/oidcupstreamwatcher/oidc_upstream_watcher_test.go @@ -174,10 +174,10 @@ func TestOIDCUpstreamWatcherControllerSync(t *testing.T) { inputSecrets: []runtime.Object{}, wantErr: controllerlib.ErrSyntheticRequeue.Error(), wantLogs: []string{ - `oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="secret \"test-client-secret\" not found" "reason"="SecretNotFound" "status"="False" "type"="ClientCredentialsValid"`, + `oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="secret \"test-client-secret\" not found" "reason"="SecretNotFound" "status"="False" "type"="ClientCredentialsSecretValid"`, `oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="discovered issuer configuration" "reason"="Success" "status"="True" "type"="OIDCDiscoverySucceeded"`, `oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="additionalAuthorizeParameters parameter names are allowed" "reason"="Success" "status"="True" "type"="AdditionalAuthorizeParametersValid"`, - `oidc-upstream-observer "msg"="found failing condition" "error"="OIDCIdentityProvider has a failing condition" "message"="secret \"test-client-secret\" not found" "name"="test-name" "namespace"="test-namespace" "reason"="SecretNotFound" "type"="ClientCredentialsValid"`, + `oidc-upstream-observer "msg"="found failing condition" "error"="OIDCIdentityProvider has a failing condition" "message"="secret \"test-client-secret\" not found" "name"="test-name" "namespace"="test-namespace" "reason"="SecretNotFound" "type"="ClientCredentialsSecretValid"`, }, wantResultingCache: []*oidctestutil.TestUpstreamOIDCIdentityProvider{}, wantResultingUpstreams: []idpv1alpha1.OIDCIdentityProvider{{ @@ -187,7 +187,7 @@ func TestOIDCUpstreamWatcherControllerSync(t *testing.T) { Conditions: []metav1.Condition{ happyAdditionalAuthorizeParametersValidCondition, { - Type: "ClientCredentialsValid", + Type: "ClientCredentialsSecretValid", Status: "False", LastTransitionTime: now, Reason: "SecretNotFound", @@ -221,10 +221,10 @@ func TestOIDCUpstreamWatcherControllerSync(t *testing.T) { }}, wantErr: controllerlib.ErrSyntheticRequeue.Error(), wantLogs: []string{ - `oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="referenced Secret \"test-client-secret\" has wrong type \"some-other-type\" (should be \"secrets.pinniped.dev/oidc-client\")" "reason"="SecretWrongType" "status"="False" "type"="ClientCredentialsValid"`, + `oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="referenced Secret \"test-client-secret\" has wrong type \"some-other-type\" (should be \"secrets.pinniped.dev/oidc-client\")" "reason"="SecretWrongType" "status"="False" "type"="ClientCredentialsSecretValid"`, `oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="discovered issuer configuration" "reason"="Success" "status"="True" "type"="OIDCDiscoverySucceeded"`, `oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="additionalAuthorizeParameters parameter names are allowed" "reason"="Success" "status"="True" "type"="AdditionalAuthorizeParametersValid"`, - `oidc-upstream-observer "msg"="found failing condition" "error"="OIDCIdentityProvider has a failing condition" "message"="referenced Secret \"test-client-secret\" has wrong type \"some-other-type\" (should be \"secrets.pinniped.dev/oidc-client\")" "name"="test-name" "namespace"="test-namespace" "reason"="SecretWrongType" "type"="ClientCredentialsValid"`, + `oidc-upstream-observer "msg"="found failing condition" "error"="OIDCIdentityProvider has a failing condition" "message"="referenced Secret \"test-client-secret\" has wrong type \"some-other-type\" (should be \"secrets.pinniped.dev/oidc-client\")" "name"="test-name" "namespace"="test-namespace" "reason"="SecretWrongType" "type"="ClientCredentialsSecretValid"`, }, wantResultingCache: []*oidctestutil.TestUpstreamOIDCIdentityProvider{}, wantResultingUpstreams: []idpv1alpha1.OIDCIdentityProvider{{ @@ -234,7 +234,7 @@ func TestOIDCUpstreamWatcherControllerSync(t *testing.T) { Conditions: []metav1.Condition{ happyAdditionalAuthorizeParametersValidCondition, { - Type: "ClientCredentialsValid", + Type: "ClientCredentialsSecretValid", Status: "False", LastTransitionTime: now, Reason: "SecretWrongType", @@ -267,10 +267,10 @@ func TestOIDCUpstreamWatcherControllerSync(t *testing.T) { }}, wantErr: controllerlib.ErrSyntheticRequeue.Error(), wantLogs: []string{ - `oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="referenced Secret \"test-client-secret\" is missing required keys [\"clientID\" \"clientSecret\"]" "reason"="SecretMissingKeys" "status"="False" "type"="ClientCredentialsValid"`, + `oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="referenced Secret \"test-client-secret\" is missing required keys [\"clientID\" \"clientSecret\"]" "reason"="SecretMissingKeys" "status"="False" "type"="ClientCredentialsSecretValid"`, `oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="discovered issuer configuration" "reason"="Success" "status"="True" "type"="OIDCDiscoverySucceeded"`, `oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="additionalAuthorizeParameters parameter names are allowed" "reason"="Success" "status"="True" "type"="AdditionalAuthorizeParametersValid"`, - `oidc-upstream-observer "msg"="found failing condition" "error"="OIDCIdentityProvider has a failing condition" "message"="referenced Secret \"test-client-secret\" is missing required keys [\"clientID\" \"clientSecret\"]" "name"="test-name" "namespace"="test-namespace" "reason"="SecretMissingKeys" "type"="ClientCredentialsValid"`, + `oidc-upstream-observer "msg"="found failing condition" "error"="OIDCIdentityProvider has a failing condition" "message"="referenced Secret \"test-client-secret\" is missing required keys [\"clientID\" \"clientSecret\"]" "name"="test-name" "namespace"="test-namespace" "reason"="SecretMissingKeys" "type"="ClientCredentialsSecretValid"`, }, wantResultingCache: []*oidctestutil.TestUpstreamOIDCIdentityProvider{}, wantResultingUpstreams: []idpv1alpha1.OIDCIdentityProvider{{ @@ -280,7 +280,7 @@ func TestOIDCUpstreamWatcherControllerSync(t *testing.T) { Conditions: []metav1.Condition{ happyAdditionalAuthorizeParametersValidCondition, { - Type: "ClientCredentialsValid", + Type: "ClientCredentialsSecretValid", Status: "False", LastTransitionTime: now, Reason: "SecretMissingKeys", @@ -316,7 +316,7 @@ func TestOIDCUpstreamWatcherControllerSync(t *testing.T) { }}, wantErr: controllerlib.ErrSyntheticRequeue.Error(), wantLogs: []string{ - `oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="loaded client credentials" "reason"="Success" "status"="True" "type"="ClientCredentialsValid"`, + `oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="loaded client credentials" "reason"="Success" "status"="True" "type"="ClientCredentialsSecretValid"`, `oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="spec.certificateAuthorityData is invalid: illegal base64 data at input byte 7" "reason"="InvalidTLSConfig" "status"="False" "type"="OIDCDiscoverySucceeded"`, `oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="additionalAuthorizeParameters parameter names are allowed" "reason"="Success" "status"="True" "type"="AdditionalAuthorizeParametersValid"`, `oidc-upstream-observer "msg"="found failing condition" "error"="OIDCIdentityProvider has a failing condition" "message"="spec.certificateAuthorityData is invalid: illegal base64 data at input byte 7" "name"="test-name" "namespace"="test-namespace" "reason"="InvalidTLSConfig" "type"="OIDCDiscoverySucceeded"`, @@ -329,7 +329,7 @@ func TestOIDCUpstreamWatcherControllerSync(t *testing.T) { Conditions: []metav1.Condition{ happyAdditionalAuthorizeParametersValidCondition, { - Type: "ClientCredentialsValid", + Type: "ClientCredentialsSecretValid", Status: "True", LastTransitionTime: now, Reason: "Success", @@ -365,7 +365,7 @@ func TestOIDCUpstreamWatcherControllerSync(t *testing.T) { }}, wantErr: controllerlib.ErrSyntheticRequeue.Error(), wantLogs: []string{ - `oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="loaded client credentials" "reason"="Success" "status"="True" "type"="ClientCredentialsValid"`, + `oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="loaded client credentials" "reason"="Success" "status"="True" "type"="ClientCredentialsSecretValid"`, `oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="spec.certificateAuthorityData is invalid: no certificates found" "reason"="InvalidTLSConfig" "status"="False" "type"="OIDCDiscoverySucceeded"`, `oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="additionalAuthorizeParameters parameter names are allowed" "reason"="Success" "status"="True" "type"="AdditionalAuthorizeParametersValid"`, `oidc-upstream-observer "msg"="found failing condition" "error"="OIDCIdentityProvider has a failing condition" "message"="spec.certificateAuthorityData is invalid: no certificates found" "name"="test-name" "namespace"="test-namespace" "reason"="InvalidTLSConfig" "type"="OIDCDiscoverySucceeded"`, @@ -378,7 +378,7 @@ func TestOIDCUpstreamWatcherControllerSync(t *testing.T) { Conditions: []metav1.Condition{ happyAdditionalAuthorizeParametersValidCondition, { - Type: "ClientCredentialsValid", + Type: "ClientCredentialsSecretValid", Status: "True", LastTransitionTime: now, Reason: "Success", @@ -411,7 +411,7 @@ func TestOIDCUpstreamWatcherControllerSync(t *testing.T) { }}, wantErr: controllerlib.ErrSyntheticRequeue.Error(), wantLogs: []string{ - `oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="loaded client credentials" "reason"="Success" "status"="True" "type"="ClientCredentialsValid"`, + `oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="loaded client credentials" "reason"="Success" "status"="True" "type"="ClientCredentialsSecretValid"`, `oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="failed to parse issuer URL: parse \"%invalid-url-that-is-really-really-long-nanananananananannanananan-batman-nanananananananananananananana-batman-lalalalalalalalalal-batman-weeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee\": invalid URL escape \"%in\"" "reason"="Unreachable" "status"="False" "type"="OIDCDiscoverySucceeded"`, `oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="additionalAuthorizeParameters parameter names are allowed" "reason"="Success" "status"="True" "type"="AdditionalAuthorizeParametersValid"`, `oidc-upstream-observer "msg"="found failing condition" "error"="OIDCIdentityProvider has a failing condition" "message"="failed to parse issuer URL: parse \"%invalid-url-that-is-really-really-long-nanananananananannanananan-batman-nanananananananananananananana-batman-lalalalalalalalalal-batman-weeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee\": invalid URL escape \"%in\"" "name"="test-name" "namespace"="test-namespace" "reason"="Unreachable" "type"="OIDCDiscoverySucceeded"`, @@ -424,7 +424,7 @@ func TestOIDCUpstreamWatcherControllerSync(t *testing.T) { Conditions: []metav1.Condition{ happyAdditionalAuthorizeParametersValidCondition, { - Type: "ClientCredentialsValid", + Type: "ClientCredentialsSecretValid", Status: "True", LastTransitionTime: now, Reason: "Success", @@ -457,7 +457,7 @@ func TestOIDCUpstreamWatcherControllerSync(t *testing.T) { }}, wantErr: controllerlib.ErrSyntheticRequeue.Error(), wantLogs: []string{ - `oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="loaded client credentials" "reason"="Success" "status"="True" "type"="ClientCredentialsValid"`, + `oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="loaded client credentials" "reason"="Success" "status"="True" "type"="ClientCredentialsSecretValid"`, `oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="issuer URL '` + strings.Replace(testIssuerURL, "https", "http", 1) + `' must have \"https\" scheme, not \"http\"" "reason"="Unreachable" "status"="False" "type"="OIDCDiscoverySucceeded"`, `oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="additionalAuthorizeParameters parameter names are allowed" "reason"="Success" "status"="True" "type"="AdditionalAuthorizeParametersValid"`, `oidc-upstream-observer "msg"="found failing condition" "error"="OIDCIdentityProvider has a failing condition" "message"="issuer URL '` + strings.Replace(testIssuerURL, "https", "http", 1) + `' must have \"https\" scheme, not \"http\"" "name"="test-name" "namespace"="test-namespace" "reason"="Unreachable" "type"="OIDCDiscoverySucceeded"`, @@ -470,7 +470,7 @@ func TestOIDCUpstreamWatcherControllerSync(t *testing.T) { Conditions: []metav1.Condition{ happyAdditionalAuthorizeParametersValidCondition, { - Type: "ClientCredentialsValid", + Type: "ClientCredentialsSecretValid", Status: "True", LastTransitionTime: now, Reason: "Success", @@ -503,7 +503,7 @@ func TestOIDCUpstreamWatcherControllerSync(t *testing.T) { }}, wantErr: controllerlib.ErrSyntheticRequeue.Error(), wantLogs: []string{ - `oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="loaded client credentials" "reason"="Success" "status"="True" "type"="ClientCredentialsValid"`, + `oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="loaded client credentials" "reason"="Success" "status"="True" "type"="ClientCredentialsSecretValid"`, `oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="issuer URL '` + testIssuerURL + "?sub=foo" + `' cannot contain query or fragment component" "reason"="Unreachable" "status"="False" "type"="OIDCDiscoverySucceeded"`, `oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="additionalAuthorizeParameters parameter names are allowed" "reason"="Success" "status"="True" "type"="AdditionalAuthorizeParametersValid"`, `oidc-upstream-observer "msg"="found failing condition" "error"="OIDCIdentityProvider has a failing condition" "message"="issuer URL '` + testIssuerURL + "?sub=foo" + `' cannot contain query or fragment component" "name"="test-name" "namespace"="test-namespace" "reason"="Unreachable" "type"="OIDCDiscoverySucceeded"`, @@ -516,7 +516,7 @@ func TestOIDCUpstreamWatcherControllerSync(t *testing.T) { Conditions: []metav1.Condition{ happyAdditionalAuthorizeParametersValidCondition, { - Type: "ClientCredentialsValid", + Type: "ClientCredentialsSecretValid", Status: "True", LastTransitionTime: now, Reason: "Success", @@ -549,7 +549,7 @@ func TestOIDCUpstreamWatcherControllerSync(t *testing.T) { }}, wantErr: controllerlib.ErrSyntheticRequeue.Error(), wantLogs: []string{ - `oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="loaded client credentials" "reason"="Success" "status"="True" "type"="ClientCredentialsValid"`, + `oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="loaded client credentials" "reason"="Success" "status"="True" "type"="ClientCredentialsSecretValid"`, `oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="issuer URL '` + testIssuerURL + "#fragment" + `' cannot contain query or fragment component" "reason"="Unreachable" "status"="False" "type"="OIDCDiscoverySucceeded"`, `oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="additionalAuthorizeParameters parameter names are allowed" "reason"="Success" "status"="True" "type"="AdditionalAuthorizeParametersValid"`, `oidc-upstream-observer "msg"="found failing condition" "error"="OIDCIdentityProvider has a failing condition" "message"="issuer URL '` + testIssuerURL + "#fragment" + `' cannot contain query or fragment component" "name"="test-name" "namespace"="test-namespace" "reason"="Unreachable" "type"="OIDCDiscoverySucceeded"`, @@ -562,7 +562,7 @@ func TestOIDCUpstreamWatcherControllerSync(t *testing.T) { Conditions: []metav1.Condition{ happyAdditionalAuthorizeParametersValidCondition, { - Type: "ClientCredentialsValid", + Type: "ClientCredentialsSecretValid", Status: "True", LastTransitionTime: now, Reason: "Success", @@ -597,7 +597,7 @@ func TestOIDCUpstreamWatcherControllerSync(t *testing.T) { wantErr: controllerlib.ErrSyntheticRequeue.Error(), wantLogs: []string{ `oidc-upstream-observer "msg"="failed to perform OIDC discovery" "error"="Get \"` + testIssuerURL + `/valid-url-that-is-really-really-long-nanananananananannanananan-batman-nanananananananananananananana-batman-lalalalalalalalalal-batman-weeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee/.well-known/openid-configuration\": tls: failed to verify certificate: x509: certificate signed by unknown authority" "issuer"="` + testIssuerURL + `/valid-url-that-is-really-really-long-nanananananananannanananan-batman-nanananananananananananananana-batman-lalalalalalalalalal-batman-weeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee" "name"="test-name" "namespace"="test-namespace"`, - `oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="loaded client credentials" "reason"="Success" "status"="True" "type"="ClientCredentialsValid"`, + `oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="loaded client credentials" "reason"="Success" "status"="True" "type"="ClientCredentialsSecretValid"`, `oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="failed to perform OIDC discovery against \"` + testIssuerURL + `/valid-url-that-is-really-really-long-nanananananananannanananan-batman-nanananananananananananananana-batman-lalalalalalalalalal-batman-weeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee\":\nGet \"` + testIssuerURL + `/valid-url-that-is-really-really-long-nanananananananannanananan-batman-nanananananananananananananana-batman-lalalalalalalalalal-batman-weeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee/.well-known/openid-configuration\": tls: failed to verify certificate: x509: certificate signed by unknown authority" "reason"="Unreachable" "status"="False" "type"="OIDCDiscoverySucceeded"`, `oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="additionalAuthorizeParameters parameter names are allowed" "reason"="Success" "status"="True" "type"="AdditionalAuthorizeParametersValid"`, `oidc-upstream-observer "msg"="found failing condition" "error"="OIDCIdentityProvider has a failing condition" "message"="failed to perform OIDC discovery against \"` + testIssuerURL + `/valid-url-that-is-really-really-long-nanananananananannanananan-batman-nanananananananananananananana-batman-lalalalalalalalalal-batman-weeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee\":\nGet \"` + testIssuerURL + `/valid-url-that-is-really-really-long-nanananananananannanananan-batman-nanananananananananananananana-batman-lalalalalalalalalal-batman-weeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee/.well-known/openid-configuration\": tls: failed to verify certificate: x509: certificate signed by unknown authority" "name"="test-name" "namespace"="test-namespace" "reason"="Unreachable" "type"="OIDCDiscoverySucceeded"`, @@ -610,7 +610,7 @@ func TestOIDCUpstreamWatcherControllerSync(t *testing.T) { Conditions: []metav1.Condition{ happyAdditionalAuthorizeParametersValidCondition, { - Type: "ClientCredentialsValid", + Type: "ClientCredentialsSecretValid", Status: "True", LastTransitionTime: now, Reason: "Success", @@ -645,7 +645,7 @@ Get "` + testIssuerURL + `/valid-url-that-is-really-really-long-nananananananana }}, wantErr: controllerlib.ErrSyntheticRequeue.Error(), wantLogs: []string{ - `oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="loaded client credentials" "reason"="Success" "status"="True" "type"="ClientCredentialsValid"`, + `oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="loaded client credentials" "reason"="Success" "status"="True" "type"="ClientCredentialsSecretValid"`, `oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="failed to parse authorization endpoint URL: parse \"%\": invalid URL escape \"%\"" "reason"="InvalidResponse" "status"="False" "type"="OIDCDiscoverySucceeded"`, `oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="additionalAuthorizeParameters parameter names are allowed" "reason"="Success" "status"="True" "type"="AdditionalAuthorizeParametersValid"`, `oidc-upstream-observer "msg"="found failing condition" "error"="OIDCIdentityProvider has a failing condition" "message"="failed to parse authorization endpoint URL: parse \"%\": invalid URL escape \"%\"" "name"="test-name" "namespace"="test-namespace" "reason"="InvalidResponse" "type"="OIDCDiscoverySucceeded"`, @@ -658,7 +658,7 @@ Get "` + testIssuerURL + `/valid-url-that-is-really-really-long-nananananananana Conditions: []metav1.Condition{ happyAdditionalAuthorizeParametersValidCondition, { - Type: "ClientCredentialsValid", + Type: "ClientCredentialsSecretValid", Status: "True", LastTransitionTime: now, Reason: "Success", @@ -692,7 +692,7 @@ Get "` + testIssuerURL + `/valid-url-that-is-really-really-long-nananananananana }}, wantErr: controllerlib.ErrSyntheticRequeue.Error(), wantLogs: []string{ - `oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="loaded client credentials" "reason"="Success" "status"="True" "type"="ClientCredentialsValid"`, + `oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="loaded client credentials" "reason"="Success" "status"="True" "type"="ClientCredentialsSecretValid"`, `oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="failed to parse revocation endpoint URL: parse \"%\": invalid URL escape \"%\"" "reason"="InvalidResponse" "status"="False" "type"="OIDCDiscoverySucceeded"`, `oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="additionalAuthorizeParameters parameter names are allowed" "reason"="Success" "status"="True" "type"="AdditionalAuthorizeParametersValid"`, `oidc-upstream-observer "msg"="found failing condition" "error"="OIDCIdentityProvider has a failing condition" "message"="failed to parse revocation endpoint URL: parse \"%\": invalid URL escape \"%\"" "name"="test-name" "namespace"="test-namespace" "reason"="InvalidResponse" "type"="OIDCDiscoverySucceeded"`, @@ -705,7 +705,7 @@ Get "` + testIssuerURL + `/valid-url-that-is-really-really-long-nananananananana Conditions: []metav1.Condition{ happyAdditionalAuthorizeParametersValidCondition, { - Type: "ClientCredentialsValid", + Type: "ClientCredentialsSecretValid", Status: "True", LastTransitionTime: now, Reason: "Success", @@ -739,7 +739,7 @@ Get "` + testIssuerURL + `/valid-url-that-is-really-really-long-nananananananana }}, wantErr: controllerlib.ErrSyntheticRequeue.Error(), wantLogs: []string{ - `oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="loaded client credentials" "reason"="Success" "status"="True" "type"="ClientCredentialsValid"`, + `oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="loaded client credentials" "reason"="Success" "status"="True" "type"="ClientCredentialsSecretValid"`, `oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="authorization endpoint URL 'http://example.com/authorize' must have \"https\" scheme, not \"http\"" "reason"="InvalidResponse" "status"="False" "type"="OIDCDiscoverySucceeded"`, `oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="additionalAuthorizeParameters parameter names are allowed" "reason"="Success" "status"="True" "type"="AdditionalAuthorizeParametersValid"`, `oidc-upstream-observer "msg"="found failing condition" "error"="OIDCIdentityProvider has a failing condition" "message"="authorization endpoint URL 'http://example.com/authorize' must have \"https\" scheme, not \"http\"" "name"="test-name" "namespace"="test-namespace" "reason"="InvalidResponse" "type"="OIDCDiscoverySucceeded"`, @@ -752,7 +752,7 @@ Get "` + testIssuerURL + `/valid-url-that-is-really-really-long-nananananananana Conditions: []metav1.Condition{ happyAdditionalAuthorizeParametersValidCondition, { - Type: "ClientCredentialsValid", + Type: "ClientCredentialsSecretValid", Status: "True", LastTransitionTime: now, Reason: "Success", @@ -786,7 +786,7 @@ Get "` + testIssuerURL + `/valid-url-that-is-really-really-long-nananananananana }}, wantErr: controllerlib.ErrSyntheticRequeue.Error(), wantLogs: []string{ - `oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="loaded client credentials" "reason"="Success" "status"="True" "type"="ClientCredentialsValid"`, + `oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="loaded client credentials" "reason"="Success" "status"="True" "type"="ClientCredentialsSecretValid"`, `oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="revocation endpoint URL 'http://example.com/revoke' must have \"https\" scheme, not \"http\"" "reason"="InvalidResponse" "status"="False" "type"="OIDCDiscoverySucceeded"`, `oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="additionalAuthorizeParameters parameter names are allowed" "reason"="Success" "status"="True" "type"="AdditionalAuthorizeParametersValid"`, `oidc-upstream-observer "msg"="found failing condition" "error"="OIDCIdentityProvider has a failing condition" "message"="revocation endpoint URL 'http://example.com/revoke' must have \"https\" scheme, not \"http\"" "name"="test-name" "namespace"="test-namespace" "reason"="InvalidResponse" "type"="OIDCDiscoverySucceeded"`, @@ -799,7 +799,7 @@ Get "` + testIssuerURL + `/valid-url-that-is-really-really-long-nananananananana Conditions: []metav1.Condition{ happyAdditionalAuthorizeParametersValidCondition, { - Type: "ClientCredentialsValid", + Type: "ClientCredentialsSecretValid", Status: "True", LastTransitionTime: now, Reason: "Success", @@ -833,7 +833,7 @@ Get "` + testIssuerURL + `/valid-url-that-is-really-really-long-nananananananana }}, wantErr: controllerlib.ErrSyntheticRequeue.Error(), wantLogs: []string{ - `oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="loaded client credentials" "reason"="Success" "status"="True" "type"="ClientCredentialsValid"`, + `oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="loaded client credentials" "reason"="Success" "status"="True" "type"="ClientCredentialsSecretValid"`, `oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="token endpoint URL 'http://example.com/token' must have \"https\" scheme, not \"http\"" "reason"="InvalidResponse" "status"="False" "type"="OIDCDiscoverySucceeded"`, `oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="additionalAuthorizeParameters parameter names are allowed" "reason"="Success" "status"="True" "type"="AdditionalAuthorizeParametersValid"`, `oidc-upstream-observer "msg"="found failing condition" "error"="OIDCIdentityProvider has a failing condition" "message"="token endpoint URL 'http://example.com/token' must have \"https\" scheme, not \"http\"" "name"="test-name" "namespace"="test-namespace" "reason"="InvalidResponse" "type"="OIDCDiscoverySucceeded"`, @@ -846,7 +846,7 @@ Get "` + testIssuerURL + `/valid-url-that-is-really-really-long-nananananananana Conditions: []metav1.Condition{ happyAdditionalAuthorizeParametersValidCondition, { - Type: "ClientCredentialsValid", + Type: "ClientCredentialsSecretValid", Status: "True", LastTransitionTime: now, Reason: "Success", @@ -880,7 +880,7 @@ Get "` + testIssuerURL + `/valid-url-that-is-really-really-long-nananananananana }}, wantErr: controllerlib.ErrSyntheticRequeue.Error(), wantLogs: []string{ - `oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="loaded client credentials" "reason"="Success" "status"="True" "type"="ClientCredentialsValid"`, + `oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="loaded client credentials" "reason"="Success" "status"="True" "type"="ClientCredentialsSecretValid"`, `oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="token endpoint URL '' must have \"https\" scheme, not \"\"" "reason"="InvalidResponse" "status"="False" "type"="OIDCDiscoverySucceeded"`, `oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="additionalAuthorizeParameters parameter names are allowed" "reason"="Success" "status"="True" "type"="AdditionalAuthorizeParametersValid"`, `oidc-upstream-observer "msg"="found failing condition" "error"="OIDCIdentityProvider has a failing condition" "message"="token endpoint URL '' must have \"https\" scheme, not \"\"" "name"="test-name" "namespace"="test-namespace" "reason"="InvalidResponse" "type"="OIDCDiscoverySucceeded"`, @@ -893,7 +893,7 @@ Get "` + testIssuerURL + `/valid-url-that-is-really-really-long-nananananananana Conditions: []metav1.Condition{ happyAdditionalAuthorizeParametersValidCondition, { - Type: "ClientCredentialsValid", + Type: "ClientCredentialsSecretValid", Status: "True", LastTransitionTime: now, Reason: "Success", @@ -927,7 +927,7 @@ Get "` + testIssuerURL + `/valid-url-that-is-really-really-long-nananananananana }}, wantErr: controllerlib.ErrSyntheticRequeue.Error(), wantLogs: []string{ - `oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="loaded client credentials" "reason"="Success" "status"="True" "type"="ClientCredentialsValid"`, + `oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="loaded client credentials" "reason"="Success" "status"="True" "type"="ClientCredentialsSecretValid"`, `oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="authorization endpoint URL '' must have \"https\" scheme, not \"\"" "reason"="InvalidResponse" "status"="False" "type"="OIDCDiscoverySucceeded"`, `oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="additionalAuthorizeParameters parameter names are allowed" "reason"="Success" "status"="True" "type"="AdditionalAuthorizeParametersValid"`, `oidc-upstream-observer "msg"="found failing condition" "error"="OIDCIdentityProvider has a failing condition" "message"="authorization endpoint URL '' must have \"https\" scheme, not \"\"" "name"="test-name" "namespace"="test-namespace" "reason"="InvalidResponse" "type"="OIDCDiscoverySucceeded"`, @@ -940,7 +940,7 @@ Get "` + testIssuerURL + `/valid-url-that-is-really-really-long-nananananananana Conditions: []metav1.Condition{ happyAdditionalAuthorizeParametersValidCondition, { - Type: "ClientCredentialsValid", + Type: "ClientCredentialsSecretValid", Status: "True", LastTransitionTime: now, Reason: "Success", @@ -974,7 +974,7 @@ Get "` + testIssuerURL + `/valid-url-that-is-really-really-long-nananananananana Status: idpv1alpha1.OIDCIdentityProviderStatus{ Phase: "Error", Conditions: []metav1.Condition{ - {Type: "ClientCredentialsValid", Status: "False", LastTransitionTime: earlier, Reason: "SomeError1", Message: "some previous error 1"}, + {Type: "ClientCredentialsSecretValid", Status: "False", LastTransitionTime: earlier, Reason: "SomeError1", Message: "some previous error 1"}, {Type: "OIDCDiscoverySucceeded", Status: "False", LastTransitionTime: earlier, Reason: "SomeError2", Message: "some previous error 2"}, }, }, @@ -985,7 +985,7 @@ Get "` + testIssuerURL + `/valid-url-that-is-really-really-long-nananananananana Data: testValidSecretData, }}, wantLogs: []string{ - `oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="loaded client credentials" "reason"="Success" "status"="True" "type"="ClientCredentialsValid"`, + `oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="loaded client credentials" "reason"="Success" "status"="True" "type"="ClientCredentialsSecretValid"`, `oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="discovered issuer configuration" "reason"="Success" "status"="True" "type"="OIDCDiscoverySucceeded"`, `oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="additionalAuthorizeParameters parameter names are allowed" "reason"="Success" "status"="True" "type"="AdditionalAuthorizeParametersValid"`, }, @@ -1010,7 +1010,7 @@ Get "` + testIssuerURL + `/valid-url-that-is-really-really-long-nananananananana Phase: "Ready", Conditions: []metav1.Condition{ happyAdditionalAuthorizeParametersValidCondition, - {Type: "ClientCredentialsValid", Status: "True", LastTransitionTime: now, Reason: "Success", Message: "loaded client credentials"}, + {Type: "ClientCredentialsSecretValid", Status: "True", LastTransitionTime: now, Reason: "Success", Message: "loaded client credentials"}, {Type: "OIDCDiscoverySucceeded", Status: "True", LastTransitionTime: now, Reason: "Success", Message: "discovered issuer configuration"}, }, }, @@ -1030,7 +1030,7 @@ Get "` + testIssuerURL + `/valid-url-that-is-really-really-long-nananananananana Phase: "Ready", Conditions: []metav1.Condition{ happyAdditionalAuthorizeParametersValidConditionEarlier, - {Type: "ClientCredentialsValid", Status: "True", LastTransitionTime: earlier, Reason: "Success", Message: "loaded client credentials"}, + {Type: "ClientCredentialsSecretValid", Status: "True", LastTransitionTime: earlier, Reason: "Success", Message: "loaded client credentials"}, {Type: "OIDCDiscoverySucceeded", Status: "True", LastTransitionTime: earlier, Reason: "Success", Message: "discovered issuer configuration"}, }, }, @@ -1041,7 +1041,7 @@ Get "` + testIssuerURL + `/valid-url-that-is-really-really-long-nananananananana Data: testValidSecretData, }}, wantLogs: []string{ - `oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="loaded client credentials" "reason"="Success" "status"="True" "type"="ClientCredentialsValid"`, + `oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="loaded client credentials" "reason"="Success" "status"="True" "type"="ClientCredentialsSecretValid"`, `oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="discovered issuer configuration" "reason"="Success" "status"="True" "type"="OIDCDiscoverySucceeded"`, `oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="additionalAuthorizeParameters parameter names are allowed" "reason"="Success" "status"="True" "type"="AdditionalAuthorizeParametersValid"`, }, @@ -1066,7 +1066,7 @@ Get "` + testIssuerURL + `/valid-url-that-is-really-really-long-nananananananana Phase: "Ready", Conditions: []metav1.Condition{ {Type: "AdditionalAuthorizeParametersValid", Status: "True", LastTransitionTime: earlier, Reason: "Success", Message: "additionalAuthorizeParameters parameter names are allowed", ObservedGeneration: 1234}, - {Type: "ClientCredentialsValid", Status: "True", LastTransitionTime: earlier, Reason: "Success", Message: "loaded client credentials", ObservedGeneration: 1234}, + {Type: "ClientCredentialsSecretValid", Status: "True", LastTransitionTime: earlier, Reason: "Success", Message: "loaded client credentials", ObservedGeneration: 1234}, {Type: "OIDCDiscoverySucceeded", Status: "True", LastTransitionTime: earlier, Reason: "Success", Message: "discovered issuer configuration", ObservedGeneration: 1234}, }, }, @@ -1086,7 +1086,7 @@ Get "` + testIssuerURL + `/valid-url-that-is-really-really-long-nananananananana Phase: "Ready", Conditions: []metav1.Condition{ happyAdditionalAuthorizeParametersValidConditionEarlier, - {Type: "ClientCredentialsValid", Status: "True", LastTransitionTime: earlier, Reason: "Success", Message: "loaded client credentials"}, + {Type: "ClientCredentialsSecretValid", Status: "True", LastTransitionTime: earlier, Reason: "Success", Message: "loaded client credentials"}, {Type: "OIDCDiscoverySucceeded", Status: "True", LastTransitionTime: earlier, Reason: "Success", Message: "discovered issuer configuration"}, }, }, @@ -1097,7 +1097,7 @@ Get "` + testIssuerURL + `/valid-url-that-is-really-really-long-nananananananana Data: testValidSecretData, }}, wantLogs: []string{ - `oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="loaded client credentials" "reason"="Success" "status"="True" "type"="ClientCredentialsValid"`, + `oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="loaded client credentials" "reason"="Success" "status"="True" "type"="ClientCredentialsSecretValid"`, `oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="discovered issuer configuration" "reason"="Success" "status"="True" "type"="OIDCDiscoverySucceeded"`, `oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="additionalAuthorizeParameters parameter names are allowed" "reason"="Success" "status"="True" "type"="AdditionalAuthorizeParametersValid"`, }, @@ -1122,7 +1122,7 @@ Get "` + testIssuerURL + `/valid-url-that-is-really-really-long-nananananananana Phase: "Ready", Conditions: []metav1.Condition{ {Type: "AdditionalAuthorizeParametersValid", Status: "True", LastTransitionTime: earlier, Reason: "Success", Message: "additionalAuthorizeParameters parameter names are allowed", ObservedGeneration: 1234}, - {Type: "ClientCredentialsValid", Status: "True", LastTransitionTime: earlier, Reason: "Success", Message: "loaded client credentials", ObservedGeneration: 1234}, + {Type: "ClientCredentialsSecretValid", Status: "True", LastTransitionTime: earlier, Reason: "Success", Message: "loaded client credentials", ObservedGeneration: 1234}, {Type: "OIDCDiscoverySucceeded", Status: "True", LastTransitionTime: earlier, Reason: "Success", Message: "discovered issuer configuration", ObservedGeneration: 1234}, }, }, @@ -1145,7 +1145,7 @@ Get "` + testIssuerURL + `/valid-url-that-is-really-really-long-nananananananana Phase: "Ready", Conditions: []metav1.Condition{ happyAdditionalAuthorizeParametersValidConditionEarlier, - {Type: "ClientCredentialsValid", Status: "True", LastTransitionTime: earlier, Reason: "Success", Message: "loaded client credentials"}, + {Type: "ClientCredentialsSecretValid", Status: "True", LastTransitionTime: earlier, Reason: "Success", Message: "loaded client credentials"}, {Type: "OIDCDiscoverySucceeded", Status: "True", LastTransitionTime: earlier, Reason: "Success", Message: "discovered issuer configuration"}, }, }, @@ -1156,7 +1156,7 @@ Get "` + testIssuerURL + `/valid-url-that-is-really-really-long-nananananananana Data: testValidSecretData, }}, wantLogs: []string{ - `oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="loaded client credentials" "reason"="Success" "status"="True" "type"="ClientCredentialsValid"`, + `oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="loaded client credentials" "reason"="Success" "status"="True" "type"="ClientCredentialsSecretValid"`, `oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="discovered issuer configuration" "reason"="Success" "status"="True" "type"="OIDCDiscoverySucceeded"`, `oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="additionalAuthorizeParameters parameter names are allowed" "reason"="Success" "status"="True" "type"="AdditionalAuthorizeParametersValid"`, }, @@ -1181,7 +1181,7 @@ Get "` + testIssuerURL + `/valid-url-that-is-really-really-long-nananananananana Phase: "Ready", Conditions: []metav1.Condition{ {Type: "AdditionalAuthorizeParametersValid", Status: "True", LastTransitionTime: earlier, Reason: "Success", Message: "additionalAuthorizeParameters parameter names are allowed", ObservedGeneration: 1234}, - {Type: "ClientCredentialsValid", Status: "True", LastTransitionTime: earlier, Reason: "Success", Message: "loaded client credentials", ObservedGeneration: 1234}, + {Type: "ClientCredentialsSecretValid", Status: "True", LastTransitionTime: earlier, Reason: "Success", Message: "loaded client credentials", ObservedGeneration: 1234}, {Type: "OIDCDiscoverySucceeded", Status: "True", LastTransitionTime: earlier, Reason: "Success", Message: "discovered issuer configuration", ObservedGeneration: 1234}, }, }, @@ -1212,7 +1212,7 @@ Get "` + testIssuerURL + `/valid-url-that-is-really-really-long-nananananananana Phase: "Ready", Conditions: []metav1.Condition{ happyAdditionalAuthorizeParametersValidConditionEarlier, - {Type: "ClientCredentialsValid", Status: "True", LastTransitionTime: earlier, Reason: "Success", Message: "loaded client credentials"}, + {Type: "ClientCredentialsSecretValid", Status: "True", LastTransitionTime: earlier, Reason: "Success", Message: "loaded client credentials"}, {Type: "OIDCDiscoverySucceeded", Status: "True", LastTransitionTime: earlier, Reason: "Success", Message: "discovered issuer configuration"}, }, }, @@ -1223,7 +1223,7 @@ Get "` + testIssuerURL + `/valid-url-that-is-really-really-long-nananananananana Data: testValidSecretData, }}, wantLogs: []string{ - `oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="loaded client credentials" "reason"="Success" "status"="True" "type"="ClientCredentialsValid"`, + `oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="loaded client credentials" "reason"="Success" "status"="True" "type"="ClientCredentialsSecretValid"`, `oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="discovered issuer configuration" "reason"="Success" "status"="True" "type"="OIDCDiscoverySucceeded"`, `oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="additionalAuthorizeParameters parameter names are allowed" "reason"="Success" "status"="True" "type"="AdditionalAuthorizeParametersValid"`, }, @@ -1250,7 +1250,7 @@ Get "` + testIssuerURL + `/valid-url-that-is-really-really-long-nananananananana Phase: "Ready", Conditions: []metav1.Condition{ {Type: "AdditionalAuthorizeParametersValid", Status: "True", LastTransitionTime: earlier, Reason: "Success", Message: "additionalAuthorizeParameters parameter names are allowed", ObservedGeneration: 1234}, - {Type: "ClientCredentialsValid", Status: "True", LastTransitionTime: earlier, Reason: "Success", Message: "loaded client credentials", ObservedGeneration: 1234}, + {Type: "ClientCredentialsSecretValid", Status: "True", LastTransitionTime: earlier, Reason: "Success", Message: "loaded client credentials", ObservedGeneration: 1234}, {Type: "OIDCDiscoverySucceeded", Status: "True", LastTransitionTime: earlier, Reason: "Success", Message: "discovered issuer configuration", ObservedGeneration: 1234}, }, }, @@ -1287,7 +1287,7 @@ Get "` + testIssuerURL + `/valid-url-that-is-really-really-long-nananananananana }}, wantErr: controllerlib.ErrSyntheticRequeue.Error(), wantLogs: []string{ - `oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="loaded client credentials" "reason"="Success" "status"="True" "type"="ClientCredentialsValid"`, + `oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="loaded client credentials" "reason"="Success" "status"="True" "type"="ClientCredentialsSecretValid"`, `oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="discovered issuer configuration" "reason"="Success" "status"="True" "type"="OIDCDiscoverySucceeded"`, `oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="the following additionalAuthorizeParameters are not allowed: response_type,scope,client_id,state,nonce,code_challenge,code_challenge_method,redirect_uri,hd" "reason"="DisallowedParameterName" "status"="False" "type"="AdditionalAuthorizeParametersValid"`, `oidc-upstream-observer "msg"="found failing condition" "error"="OIDCIdentityProvider has a failing condition" "message"="the following additionalAuthorizeParameters are not allowed: response_type,scope,client_id,state,nonce,code_challenge,code_challenge_method,redirect_uri,hd" "name"="test-name" "namespace"="test-namespace" "reason"="DisallowedParameterName" "type"="AdditionalAuthorizeParametersValid"`, @@ -1301,7 +1301,7 @@ Get "` + testIssuerURL + `/valid-url-that-is-really-really-long-nananananananana {Type: "AdditionalAuthorizeParametersValid", Status: "False", LastTransitionTime: now, Reason: "DisallowedParameterName", Message: "the following additionalAuthorizeParameters are not allowed: " + "response_type,scope,client_id,state,nonce,code_challenge,code_challenge_method,redirect_uri,hd", ObservedGeneration: 1234}, - {Type: "ClientCredentialsValid", Status: "True", LastTransitionTime: now, Reason: "Success", Message: "loaded client credentials", ObservedGeneration: 1234}, + {Type: "ClientCredentialsSecretValid", Status: "True", LastTransitionTime: now, Reason: "Success", Message: "loaded client credentials", ObservedGeneration: 1234}, {Type: "OIDCDiscoverySucceeded", Status: "True", LastTransitionTime: now, Reason: "Success", Message: "discovered issuer configuration", ObservedGeneration: 1234}, }, }, @@ -1325,7 +1325,7 @@ Get "` + testIssuerURL + `/valid-url-that-is-really-really-long-nananananananana wantErr: controllerlib.ErrSyntheticRequeue.Error(), wantLogs: []string{ `oidc-upstream-observer "msg"="failed to perform OIDC discovery" "error"="oidc: issuer did not match the issuer returned by provider, expected \"` + testIssuerURL + `/ends-with-slash\" got \"` + testIssuerURL + `/ends-with-slash/\"" "issuer"="` + testIssuerURL + `/ends-with-slash" "name"="test-name" "namespace"="test-namespace"`, - `oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="loaded client credentials" "reason"="Success" "status"="True" "type"="ClientCredentialsValid"`, + `oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="loaded client credentials" "reason"="Success" "status"="True" "type"="ClientCredentialsSecretValid"`, `oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="failed to perform OIDC discovery against \"` + testIssuerURL + `/ends-with-slash\":\noidc: issuer did not match the issuer returned by provider, expected \"` + testIssuerURL + `/ends-with-slash\" got \"` + testIssuerURL + `/ends-with-slash/\"" "reason"="Unreachable" "status"="False" "type"="OIDCDiscoverySucceeded"`, `oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="additionalAuthorizeParameters parameter names are allowed" "reason"="Success" "status"="True" "type"="AdditionalAuthorizeParametersValid"`, `oidc-upstream-observer "msg"="found failing condition" "error"="OIDCIdentityProvider has a failing condition" "message"="failed to perform OIDC discovery against \"` + testIssuerURL + `/ends-with-slash\":\noidc: issuer did not match the issuer returned by provider, expected \"` + testIssuerURL + `/ends-with-slash\" got \"` + testIssuerURL + `/ends-with-slash/\"" "name"="test-name" "namespace"="test-namespace" "reason"="Unreachable" "type"="OIDCDiscoverySucceeded"`, @@ -1338,7 +1338,7 @@ Get "` + testIssuerURL + `/valid-url-that-is-really-really-long-nananananananana Conditions: []metav1.Condition{ happyAdditionalAuthorizeParametersValidCondition, { - Type: "ClientCredentialsValid", + Type: "ClientCredentialsSecretValid", Status: "True", LastTransitionTime: now, Reason: "Success", @@ -1374,7 +1374,7 @@ oidc: issuer did not match the issuer returned by provider, expected "` + testIs wantErr: controllerlib.ErrSyntheticRequeue.Error(), wantLogs: []string{ `oidc-upstream-observer "msg"="failed to perform OIDC discovery" "error"="oidc: issuer did not match the issuer returned by provider, expected \"` + testIssuerURL + `/\" got \"` + testIssuerURL + `\"" "issuer"="` + testIssuerURL + `/" "name"="test-name" "namespace"="test-namespace"`, - `oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="loaded client credentials" "reason"="Success" "status"="True" "type"="ClientCredentialsValid"`, + `oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="loaded client credentials" "reason"="Success" "status"="True" "type"="ClientCredentialsSecretValid"`, `oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="failed to perform OIDC discovery against \"` + testIssuerURL + `/\":\noidc: issuer did not match the issuer returned by provider, expected \"` + testIssuerURL + `/\" got \"` + testIssuerURL + `\"" "reason"="Unreachable" "status"="False" "type"="OIDCDiscoverySucceeded"`, `oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="additionalAuthorizeParameters parameter names are allowed" "reason"="Success" "status"="True" "type"="AdditionalAuthorizeParametersValid"`, `oidc-upstream-observer "msg"="found failing condition" "error"="OIDCIdentityProvider has a failing condition" "message"="failed to perform OIDC discovery against \"` + testIssuerURL + `/\":\noidc: issuer did not match the issuer returned by provider, expected \"` + testIssuerURL + `/\" got \"` + testIssuerURL + `\"" "name"="test-name" "namespace"="test-namespace" "reason"="Unreachable" "type"="OIDCDiscoverySucceeded"`, @@ -1387,7 +1387,7 @@ oidc: issuer did not match the issuer returned by provider, expected "` + testIs Conditions: []metav1.Condition{ happyAdditionalAuthorizeParametersValidCondition, { - Type: "ClientCredentialsValid", + Type: "ClientCredentialsSecretValid", Status: "True", LastTransitionTime: now, Reason: "Success", @@ -1448,7 +1448,7 @@ oidc: issuer did not match the issuer returned by provider, expected "` + testIs require.Equal(t, len(tt.wantResultingCache), len(actualIDPList)) for i := range actualIDPList { actualIDP := actualIDPList[i].(*upstreamoidc.ProviderConfig) - require.Equal(t, tt.wantResultingCache[i].GetName(), actualIDP.GetName()) + require.Equal(t, tt.wantResultingCache[i].GetResourceName(), actualIDP.GetResourceName()) require.Equal(t, tt.wantResultingCache[i].GetClientID(), actualIDP.GetClientID()) require.Equal(t, tt.wantResultingCache[i].GetAuthorizationURL().String(), actualIDP.GetAuthorizationURL().String()) require.Equal(t, tt.wantResultingCache[i].GetUsernameClaim(), actualIDP.GetUsernameClaim()) diff --git a/internal/controller/supervisorstorage/garbage_collector.go b/internal/controller/supervisorstorage/garbage_collector.go index 26285d53c..0ae8ac5ca 100644 --- a/internal/controller/supervisorstorage/garbage_collector.go +++ b/internal/controller/supervisorstorage/garbage_collector.go @@ -246,7 +246,7 @@ func (c *garbageCollectorController) tryRevokeUpstreamOIDCToken(ctx context.Cont // Try to find the provider that was originally used to create the stored session. var foundOIDCIdentityProviderI upstreamprovider.UpstreamOIDCIdentityProviderI for _, p := range c.idpCache.GetOIDCIdentityProviders() { - if p.GetName() == customSessionData.ProviderName && p.GetResourceUID() == customSessionData.ProviderUID { + if p.GetResourceName() == customSessionData.ProviderName && p.GetResourceUID() == customSessionData.ProviderUID { foundOIDCIdentityProviderI = p break } diff --git a/internal/controller/supervisorstorage/garbage_collector_test.go b/internal/controller/supervisorstorage/garbage_collector_test.go index c020ba92e..903ee7753 100644 --- a/internal/controller/supervisorstorage/garbage_collector_test.go +++ b/internal/controller/supervisorstorage/garbage_collector_test.go @@ -122,7 +122,7 @@ func TestGarbageCollectorControllerSync(t *testing.T) { spec.Run(t, "Sync", func(t *testing.T, when spec.G, it spec.S) { const ( installedInNamespace = "some-namespace" - currentSessionStorageVersion = "7" // update this when you update the storage version in the production code + currentSessionStorageVersion = "8" // update this when you update the storage version in the production code ) var ( diff --git a/internal/controller/utils.go b/internal/controller/utils.go index f63fa3dae..5b84ba109 100644 --- a/internal/controller/utils.go +++ b/internal/controller/utils.go @@ -4,9 +4,17 @@ package controller import ( + "crypto/x509" + "encoding/base64" + "fmt" + "slices" + corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/util/cert" + authv1alpha1 "go.pinniped.dev/generated/latest/apis/concierge/authentication/v1alpha1" + idpv1alpha1 "go.pinniped.dev/generated/latest/apis/supervisor/idp/v1alpha1" "go.pinniped.dev/internal/controllerlib" ) @@ -43,12 +51,16 @@ func SimpleFilter(match func(metav1.Object) bool, parentFunc controllerlib.Paren } } -func MatchAnySecretOfTypeFilter(secretType corev1.SecretType, parentFunc controllerlib.ParentFunc) controllerlib.Filter { +func MatchAnySecretOfTypeFilter(secretType corev1.SecretType, parentFunc controllerlib.ParentFunc, namespaces ...string) controllerlib.Filter { isSecretOfType := func(obj metav1.Object) bool { secret, ok := obj.(*corev1.Secret) if !ok { return false } + // Only match on namespace if namespaces are provided + if len(namespaces) > 0 && !slices.Contains(namespaces, secret.Namespace) { + return false + } return secret.Type == secretType } return SimpleFilter(isSecretOfType, parentFunc) @@ -87,3 +99,43 @@ type WithInformerOptionFunc func( // Same signature as controllerlib.WithInitialEvent(). type WithInitialEventOptionFunc func(key controllerlib.Key) controllerlib.Option + +// BuildCertPoolAuth returns a PEM-encoded CA bundle from the provided spec. If the provided spec is nil, a +// nil CA bundle will be returned. If the provided spec contains a CA bundle that is not properly +// encoded, an error will be returned. +func BuildCertPoolAuth(spec *authv1alpha1.TLSSpec) (*x509.CertPool, []byte, error) { + if spec == nil { + return nil, nil, nil + } + + return buildCertPool(spec.CertificateAuthorityData) +} + +// BuildCertPoolIDP returns a PEM-encoded CA bundle from the provided spec. If the provided spec is nil, a +// nil CA bundle will be returned. If the provided spec contains a CA bundle that is not properly +// encoded, an error will be returned. +func BuildCertPoolIDP(spec *idpv1alpha1.TLSSpec) (*x509.CertPool, []byte, error) { + if spec == nil { + return nil, nil, nil + } + + return buildCertPool(spec.CertificateAuthorityData) +} + +func buildCertPool(certificateAuthorityData string) (*x509.CertPool, []byte, error) { + if len(certificateAuthorityData) == 0 { + return nil, nil, nil + } + + pem, err := base64.StdEncoding.DecodeString(certificateAuthorityData) + if err != nil { + return nil, nil, err + } + + rootCAs, err := cert.NewPoolFromBytes(pem) + if err != nil { + return nil, nil, fmt.Errorf("certificateAuthorityData is not valid PEM: %w", err) + } + + return rootCAs, pem, nil +} diff --git a/internal/endpointaddr/endpointaddr.go b/internal/endpointaddr/endpointaddr.go index dd475e7fc..c54ca421e 100644 --- a/internal/endpointaddr/endpointaddr.go +++ b/internal/endpointaddr/endpointaddr.go @@ -18,7 +18,7 @@ import ( type HostPort struct { // Host is the validated host part of the input, which may be a hostname or IP. // - // This string can be be used as an x509 certificate SAN. + // This string can be used as a x509 certificate SAN. Host string // Port is the validated port number, which may be defaulted. diff --git a/internal/federationdomain/downstreamsession/downstream_session.go b/internal/federationdomain/downstreamsession/downstream_session.go index bac780a75..f477b1d10 100644 --- a/internal/federationdomain/downstreamsession/downstream_session.go +++ b/internal/federationdomain/downstreamsession/downstream_session.go @@ -56,7 +56,7 @@ func NewPinnipedSession( UpstreamUsername: c.UpstreamIdentity.UpstreamUsername, UpstreamGroups: c.UpstreamIdentity.UpstreamGroups, ProviderUID: idp.GetProvider().GetResourceUID(), - ProviderName: idp.GetProvider().GetName(), + ProviderName: idp.GetProvider().GetResourceName(), ProviderType: idp.GetSessionProviderType(), Warnings: c.UpstreamLoginExtras.Warnings, } diff --git a/internal/federationdomain/downstreamsubject/downstream_subject.go b/internal/federationdomain/downstreamsubject/downstream_subject.go index 5c754d9aa..345ec737e 100644 --- a/internal/federationdomain/downstreamsubject/downstream_subject.go +++ b/internal/federationdomain/downstreamsubject/downstream_subject.go @@ -24,3 +24,11 @@ func OIDC(upstreamIssuerAsString string, upstreamSubject string, idpDisplayName oidc.IDTokenClaimSubject, url.QueryEscape(upstreamSubject), ) } + +func GitHub(apiBaseURL, idpDisplayName, login, id string) string { + return fmt.Sprintf("%s?%s=%s&login=%s&id=%s", apiBaseURL, + oidc.IDTokenSubClaimIDPNameQueryParam, url.QueryEscape(idpDisplayName), + url.QueryEscape(login), + url.QueryEscape(id), + ) +} diff --git a/internal/federationdomain/downstreamsubject/downstream_subject_test.go b/internal/federationdomain/downstreamsubject/downstream_subject_test.go index b96ec85ad..3e57707ef 100644 --- a/internal/federationdomain/downstreamsubject/downstream_subject_test.go +++ b/internal/federationdomain/downstreamsubject/downstream_subject_test.go @@ -89,3 +89,41 @@ func TestOIDC(t *testing.T) { }) } } + +func TestGitHub(t *testing.T) { + tests := []struct { + name string + apiBaseURL string + idpDisplayName string + login string + id string + wantSubject string + }{ + { + name: "simple display name", + apiBaseURL: "https://github.com", + idpDisplayName: "simpleName", + login: "some login", + id: "some id", + wantSubject: "https://github.com?idpName=simpleName&login=some+login&id=some+id", + }, + { + name: "interesting display name", + apiBaseURL: "https://server.example.com:1234/path", + idpDisplayName: "this is a 👍 display name that 🦭 can handle", + login: "some other login", + id: "some other id", + wantSubject: "https://server.example.com:1234/path?idpName=this+is+a+%F0%9F%91%8D+display+name+that+%F0%9F%A6%AD+can+handle&login=some+other+login&id=some+other+id", + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + t.Parallel() + + actual := GitHub(test.apiBaseURL, test.idpDisplayName, test.login, test.id) + + require.Equal(t, test.wantSubject, actual) + }) + } +} diff --git a/internal/federationdomain/dynamicupstreamprovider/dynamic_upstream_idp_provider.go b/internal/federationdomain/dynamicupstreamprovider/dynamic_upstream_idp_provider.go index bb92e6105..ea0cd50e8 100644 --- a/internal/federationdomain/dynamicupstreamprovider/dynamic_upstream_idp_provider.go +++ b/internal/federationdomain/dynamicupstreamprovider/dynamic_upstream_idp_provider.go @@ -1,4 +1,4 @@ -// Copyright 2020-2023 the Pinniped contributors. All Rights Reserved. +// Copyright 2020-2024 the Pinniped contributors. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 package dynamicupstreamprovider @@ -17,12 +17,15 @@ type DynamicUpstreamIDPProvider interface { GetLDAPIdentityProviders() []upstreamprovider.UpstreamLDAPIdentityProviderI SetActiveDirectoryIdentityProviders(adIDPs []upstreamprovider.UpstreamLDAPIdentityProviderI) GetActiveDirectoryIdentityProviders() []upstreamprovider.UpstreamLDAPIdentityProviderI + SetGitHubIdentityProviders(gitHubIDPs []upstreamprovider.UpstreamGithubIdentityProviderI) + GetGitHubIdentityProviders() []upstreamprovider.UpstreamGithubIdentityProviderI } type dynamicUpstreamIDPProvider struct { oidcUpstreams []upstreamprovider.UpstreamOIDCIdentityProviderI ldapUpstreams []upstreamprovider.UpstreamLDAPIdentityProviderI activeDirectoryUpstreams []upstreamprovider.UpstreamLDAPIdentityProviderI + gitHubUpstreams []upstreamprovider.UpstreamGithubIdentityProviderI mutex sync.RWMutex } @@ -31,6 +34,7 @@ func NewDynamicUpstreamIDPProvider() DynamicUpstreamIDPProvider { oidcUpstreams: []upstreamprovider.UpstreamOIDCIdentityProviderI{}, ldapUpstreams: []upstreamprovider.UpstreamLDAPIdentityProviderI{}, activeDirectoryUpstreams: []upstreamprovider.UpstreamLDAPIdentityProviderI{}, + gitHubUpstreams: []upstreamprovider.UpstreamGithubIdentityProviderI{}, } } @@ -70,6 +74,18 @@ func (p *dynamicUpstreamIDPProvider) GetActiveDirectoryIdentityProviders() []ups return p.activeDirectoryUpstreams } +func (p *dynamicUpstreamIDPProvider) SetGitHubIdentityProviders(gitHubIDPs []upstreamprovider.UpstreamGithubIdentityProviderI) { + p.mutex.Lock() // acquire a write lock + defer p.mutex.Unlock() + p.gitHubUpstreams = gitHubIDPs +} + +func (p *dynamicUpstreamIDPProvider) GetGitHubIdentityProviders() []upstreamprovider.UpstreamGithubIdentityProviderI { + p.mutex.RLock() // acquire a read lock + defer p.mutex.RUnlock() + return p.gitHubUpstreams +} + type RetryableRevocationError struct { wrapped error } diff --git a/internal/federationdomain/endpoints/auth/auth_handler_test.go b/internal/federationdomain/endpoints/auth/auth_handler_test.go index 8d2a3d297..8b3aed110 100644 --- a/internal/federationdomain/endpoints/auth/auth_handler_test.go +++ b/internal/federationdomain/endpoints/auth/auth_handler_test.go @@ -55,6 +55,8 @@ func TestAuthorizationEndpoint(t *testing.T) { //nolint:gocyclo ldapUpstreamResourceUID = "ldap-resource-uid" activeDirectoryUpstreamName = "some-active-directory-idp" activeDirectoryUpstreamResourceUID = "active-directory-resource-uid" + githubUpstreamName = "some-github-idp" + githubUpstreamResourceUID = "github-resource-uid" oidcUpstreamIssuer = "https://my-upstream-issuer.com" oidcUpstreamSubject = "abc123-some guid" // has a space character which should get escaped in URL @@ -291,6 +293,15 @@ func TestAuthorizationEndpoint(t *testing.T) { //nolint:gocyclo WithPasswordGrantError(errors.New("should not have used password grant on this instance")) } + upstreamGitHubIdentityProviderBuilder := func() *oidctestutil.TestUpstreamGitHubIdentityProviderBuilder { + return oidctestutil.NewTestUpstreamGitHubIdentityProviderBuilder(). + WithName(githubUpstreamName). + WithResourceUID(githubUpstreamResourceUID). + WithClientID("some-github-client-id"). + WithAuthorizationURL(upstreamAuthURL.String()). + WithScopes([]string{"scope1", "scope2"}) // the scopes to request when starting the upstream authorization flow + } + passwordGrantUpstreamOIDCIdentityProviderBuilder := func() *oidctestutil.TestUpstreamOIDCIdentityProviderBuilder { return oidctestutil.NewTestUpstreamOIDCIdentityProviderBuilder(). WithName(oidcPasswordGrantUpstreamName). @@ -463,6 +474,7 @@ func TestAuthorizationEndpoint(t *testing.T) { //nolint:gocyclo happyGetRequestPathForOIDCPasswordGrantUpstream := modifiedHappyGetRequestPath(map[string]string{"pinniped_idp_name": oidcPasswordGrantUpstreamName}) happyGetRequestPathForLDAPUpstream := modifiedHappyGetRequestPath(map[string]string{"pinniped_idp_name": ldapUpstreamName}) happyGetRequestPathForADUpstream := modifiedHappyGetRequestPath(map[string]string{"pinniped_idp_name": activeDirectoryUpstreamName}) + happyGetRequestPathForGithubUpstream := modifiedHappyGetRequestPath(map[string]string{"pinniped_idp_name": githubUpstreamName}) modifiedHappyGetRequestPathForOIDCUpstream := func(queryOverrides map[string]string) string { queryOverrides["pinniped_idp_name"] = oidcUpstreamName @@ -480,6 +492,10 @@ func TestAuthorizationEndpoint(t *testing.T) { //nolint:gocyclo queryOverrides["pinniped_idp_name"] = activeDirectoryUpstreamName return modifiedHappyGetRequestPath(queryOverrides) } + modifiedHappyGetRequestPathForGithubUpstream := func(queryOverrides map[string]string) string { + queryOverrides["pinniped_idp_name"] = githubUpstreamName + return modifiedHappyGetRequestPath(queryOverrides) + } happyGetRequestQueryMapForOIDCUpstream := modifiedQueryMap(happyGetRequestQueryMap, map[string]string{"pinniped_idp_name": oidcUpstreamName}) happyGetRequestQueryMapForOIDCPasswordGrantUpstream := modifiedQueryMap(happyGetRequestQueryMap, map[string]string{"pinniped_idp_name": oidcPasswordGrantUpstreamName}) @@ -533,6 +549,17 @@ func TestAuthorizationEndpoint(t *testing.T) { //nolint:gocyclo return urlWithQuery(upstreamAuthURL.String(), query) } + expectedRedirectLocationForUpstreamGithub := func(expectedUpstreamState string) string { + query := map[string]string{ + "response_type": "code", + "scope": "scope1 scope2", + "client_id": "some-github-client-id", + "state": expectedUpstreamState, + "redirect_uri": downstreamIssuer + "/callback", + } + return urlWithQuery(upstreamAuthURL.String(), query) + } + expectedHappyActiveDirectoryUpstreamCustomSession := &psession.CustomSessionData{ Username: happyLDAPUsernameFromAuthenticator, UpstreamUsername: happyLDAPUsernameFromAuthenticator, @@ -711,6 +738,41 @@ func TestAuthorizationEndpoint(t *testing.T) { //nolint:gocyclo wantUpstreamStateParamInLocationHeader: true, wantBodyStringWithLocationInHref: true, }, + { + name: "GitHub upstream browser flow happy path using GET without a CSRF cookie", + idps: testidplister.NewUpstreamIDPListerBuilder().WithGitHub(upstreamGitHubIdentityProviderBuilder().Build()), + generateCSRF: happyCSRFGenerator, + generatePKCE: happyPKCEGenerator, + generateNonce: happyNonceGenerator, + stateEncoder: happyStateEncoder, + cookieEncoder: happyCookieEncoder, + method: http.MethodGet, + path: happyGetRequestPathForGithubUpstream, + wantStatus: http.StatusSeeOther, + wantContentType: htmlContentType, + wantCSRFValueInCookieHeader: happyCSRF, + wantLocationHeader: expectedRedirectLocationForUpstreamGithub(expectedUpstreamStateParam(nil, "", githubUpstreamName, "github")), + wantUpstreamStateParamInLocationHeader: true, + wantBodyStringWithLocationInHref: true, + }, + { + name: "GitHub upstream browser flow happy path using GET without a CSRF cookie using a dynamic client", + idps: testidplister.NewUpstreamIDPListerBuilder().WithGitHub(upstreamGitHubIdentityProviderBuilder().Build()), + kubeResources: addFullyCapableDynamicClientAndSecretToKubeResources, + generateCSRF: happyCSRFGenerator, + generatePKCE: happyPKCEGenerator, + generateNonce: happyNonceGenerator, + stateEncoder: happyStateEncoder, + cookieEncoder: happyCookieEncoder, + method: http.MethodGet, + path: modifiedHappyGetRequestPathForGithubUpstream(map[string]string{"client_id": dynamicClientID, "scope": testutil.AllDynamicClientScopesSpaceSep}), + wantStatus: http.StatusSeeOther, + wantContentType: htmlContentType, + wantCSRFValueInCookieHeader: happyCSRF, + wantLocationHeader: expectedRedirectLocationForUpstreamGithub(expectedUpstreamStateParam(map[string]string{"client_id": dynamicClientID, "scope": testutil.AllDynamicClientScopesSpaceSep}, "", githubUpstreamName, "github")), + wantUpstreamStateParamInLocationHeader: true, + wantBodyStringWithLocationInHref: true, + }, { name: "LDAP upstream browser flow happy path using GET without a CSRF cookie", idps: testidplister.NewUpstreamIDPListerBuilder().WithLDAP(upstreamLDAPIdentityProviderBuilder().Build()), diff --git a/internal/federationdomain/endpoints/callback/callback_handler.go b/internal/federationdomain/endpoints/callback/callback_handler.go index c812d3143..a7c24dfe7 100644 --- a/internal/federationdomain/endpoints/callback/callback_handler.go +++ b/internal/federationdomain/endpoints/callback/callback_handler.go @@ -48,6 +48,9 @@ func NewHandler( authorizeRequester, err := oauthHelper.NewAuthorizeRequest(r.Context(), reconstitutedAuthRequest) if err != nil { plog.Error("error using state downstream auth params", err, + "identityProviderDisplayName", idp.GetDisplayName(), + "identityProviderResourceName", idp.GetProvider().GetResourceName(), + "supervisorCallbackURL", redirectURI, "fositeErr", oidc.FositeErrorForLog(err)) return httperr.New(http.StatusBadRequest, "error using state downstream auth params") } @@ -59,6 +62,10 @@ func NewHandler( identity, loginExtras, err := idp.LoginFromCallback(r.Context(), authcode(r), state.PKCECode, state.Nonce, redirectURI) if err != nil { + plog.InfoErr("unable to complete login from callback", err, + "identityProviderDisplayName", idp.GetDisplayName(), + "identityProviderResourceName", idp.GetProvider().GetResourceName(), + "supervisorCallbackURL", redirectURI) return err } @@ -69,13 +76,20 @@ func NewHandler( GrantedScopes: authorizeRequester.GetGrantedScopes(), }) if err != nil { + plog.InfoErr("unable to create a Pinniped session", err, + "identityProviderDisplayName", idp.GetDisplayName(), + "identityProviderResourceName", idp.GetProvider().GetResourceName(), + "supervisorCallbackURL", redirectURI) return httperr.Wrap(http.StatusUnprocessableEntity, err.Error(), err) } authorizeResponder, err := oauthHelper.NewAuthorizeResponse(r.Context(), authorizeRequester, session) if err != nil { plog.WarningErr("error while generating and saving authcode", err, - "identityProviderDisplayName", idp.GetDisplayName(), "fositeErr", oidc.FositeErrorForLog(err)) + "identityProviderDisplayName", idp.GetDisplayName(), + "identityProviderResourceName", idp.GetProvider().GetResourceName(), + "supervisorCallbackURL", redirectURI, + "fositeErr", oidc.FositeErrorForLog(err)) return httperr.Wrap(http.StatusInternalServerError, "error while generating and saving authcode", err) } diff --git a/internal/federationdomain/endpoints/callback/callback_handler_test.go b/internal/federationdomain/endpoints/callback/callback_handler_test.go index 0743c46a4..8859d7ebe 100644 --- a/internal/federationdomain/endpoints/callback/callback_handler_test.go +++ b/internal/federationdomain/endpoints/callback/callback_handler_test.go @@ -6,6 +6,7 @@ package callback import ( "context" "errors" + "fmt" "net/http" "net/http/httptest" "net/url" @@ -25,6 +26,7 @@ import ( "go.pinniped.dev/internal/federationdomain/oidc" "go.pinniped.dev/internal/federationdomain/oidcclientvalidator" "go.pinniped.dev/internal/federationdomain/storage" + "go.pinniped.dev/internal/federationdomain/upstreamprovider" "go.pinniped.dev/internal/psession" "go.pinniped.dev/internal/testutil" "go.pinniped.dev/internal/testutil/oidctestutil" @@ -35,8 +37,9 @@ import ( ) const ( - happyUpstreamIDPName = "upstream-idp-name" - happyUpstreamIDPResourceUID = "upstream-uid" + // Upstream OIDC. + happyOIDCUpstreamIDPName = "upstream-oidc-idp-name" + happyOIDCUpstreamIDPResourceUID = "upstream-oidc-resource-uid" oidcUpstreamIssuer = "https://my-upstream-issuer.com" oidcUpstreamRefreshToken = "test-refresh-token" @@ -48,12 +51,18 @@ const ( oidcUpstreamUsernameClaim = "the-user-claim" oidcUpstreamGroupsClaim = "the-groups-claim" + // Upstream GitHub. + happyGithubIDPName = "upstream-github-idp-name" + happyGithubIDPResourceUID = "upstream-github-idp-resource-uid" + + // Upstream OAuth2 (OIDC or GitHub). happyUpstreamAuthcode = "upstream-auth-code" happyUpstreamRedirectURI = "https://example.com/callback" + // Downstream parameters. happyDownstreamState = "8b-state" happyDownstreamCSRF = "test-csrf" - happyDownstreamPKCE = "test-pkce" + happyDownstreamPKCEVerifier = "test-pkce" happyDownstreamNonce = "test-nonce" happyDownstreamStateVersion = "2" @@ -73,6 +82,11 @@ const ( ) var ( + githubUpstreamUsername = "some-github-login" + githubUpstreamGroupMembership = []string{"org1/team1", "org2/team2"} + githubDownstreamSubject = fmt.Sprintf("https://github.com?idpName=%s&sub=%s", happyGithubIDPName, githubUpstreamUsername) + githubUpstreamAccessToken = "some-opaque-access-token-from-github" //nolint:gosec // this is not a credential + oidcUpstreamGroupMembership = []string{"test-pinniped-group-0", "test-pinniped-group-1"} happyDownstreamScopesRequested = []string{"openid", "username", "groups"} happyDownstreamScopesGranted = []string{"openid", "username", "groups"} @@ -94,12 +108,12 @@ var ( ) happyDownstreamRequestParamsForDynamicClient = happyDownstreamRequestParamsQueryForDynamicClient.Encode() - happyDownstreamCustomSessionData = &psession.CustomSessionData{ + happyDownstreamCustomSessionDataForOIDCUpstream = &psession.CustomSessionData{ Username: oidcUpstreamUsername, UpstreamUsername: oidcUpstreamUsername, UpstreamGroups: oidcUpstreamGroupMembership, - ProviderUID: happyUpstreamIDPResourceUID, - ProviderName: happyUpstreamIDPName, + ProviderUID: happyOIDCUpstreamIDPResourceUID, + ProviderName: happyOIDCUpstreamIDPName, ProviderType: psession.ProviderTypeOIDC, OIDC: &psession.OIDCSessionData{ UpstreamRefreshToken: oidcUpstreamRefreshToken, @@ -107,10 +121,16 @@ var ( UpstreamSubject: oidcUpstreamSubject, }, } - happyDownstreamCustomSessionDataWithUsernameAndGroups = func(wantDownstreamUsername, wantUpstreamUsername string, wantUpstreamGroups []string) *psession.CustomSessionData { - copyOfCustomSession := *happyDownstreamCustomSessionData - copyOfOIDC := *(happyDownstreamCustomSessionData.OIDC) - copyOfCustomSession.OIDC = ©OfOIDC + happyDownstreamCustomSessionDataWithUsernameAndGroups = func(startingSessionData *psession.CustomSessionData, wantDownstreamUsername, wantUpstreamUsername string, wantUpstreamGroups []string) *psession.CustomSessionData { + copyOfCustomSession := *startingSessionData + if startingSessionData.OIDC != nil { + copyOfOIDC := *(startingSessionData.OIDC) + copyOfCustomSession.OIDC = ©OfOIDC + } + if startingSessionData.GitHub != nil { + copyOfGitHub := *(startingSessionData.GitHub) + copyOfCustomSession.GitHub = ©OfGitHub + } copyOfCustomSession.Username = wantDownstreamUsername copyOfCustomSession.UpstreamUsername = wantUpstreamUsername copyOfCustomSession.UpstreamGroups = wantUpstreamGroups @@ -120,8 +140,8 @@ var ( Username: oidcUpstreamUsername, UpstreamUsername: oidcUpstreamUsername, UpstreamGroups: oidcUpstreamGroupMembership, - ProviderUID: happyUpstreamIDPResourceUID, - ProviderName: happyUpstreamIDPName, + ProviderUID: happyOIDCUpstreamIDPResourceUID, + ProviderName: happyOIDCUpstreamIDPName, ProviderType: psession.ProviderTypeOIDC, OIDC: &psession.OIDCSessionData{ UpstreamAccessToken: oidcUpstreamAccessToken, @@ -129,6 +149,17 @@ var ( UpstreamSubject: oidcUpstreamSubject, }, } + happyDownstreamCustomSessionDataForGitHubUpstream = &psession.CustomSessionData{ + Username: githubUpstreamUsername, + UpstreamUsername: githubUpstreamUsername, + UpstreamGroups: githubUpstreamGroupMembership, + ProviderUID: happyGithubIDPResourceUID, + ProviderName: happyGithubIDPName, + ProviderType: psession.ProviderTypeGitHub, + GitHub: &psession.GitHubSessionData{ + UpstreamAccessToken: githubUpstreamAccessToken, + }, + } ) func TestCallbackEndpoint(t *testing.T) { @@ -152,18 +183,25 @@ func TestCallbackEndpoint(t *testing.T) { var happyCookieCodec = securecookie.New(cookieEncoderHashKey, cookieEncoderBlockKey) happyCookieCodec.SetSerializer(securecookie.JSONEncoder{}) - happyState := happyUpstreamStateParam().Build(t, happyStateCodec) - happyStateForDynamicClient := happyUpstreamStateParamForDynamicClient().Build(t, happyStateCodec) + happyOIDCState := happyOIDCUpstreamStateParam().Build(t, happyStateCodec) + happyOIDCStateForDynamicClient := happyOIDCUpstreamStateParamForDynamicClient().Build(t, happyStateCodec) + + happyGitHubPath := newRequestPath().WithState(happyGitHubUpstreamStateParam().Build(t, happyStateCodec)).String() encodedIncomingCookieCSRFValue, err := happyCookieCodec.Encode("csrf", happyDownstreamCSRF) require.NoError(t, err) happyCSRFCookie := "__Host-pinniped-csrf=" + encodedIncomingCookieCSRFValue - happyExchangeAndValidateTokensArgs := &oidctestutil.ExchangeAuthcodeAndValidateTokenArgs{ + happyOIDCUpstreamExchangeAuthcodeAndValidateTokenArgs := &oidctestutil.ExchangeAuthcodeAndValidateTokenArgs{ Authcode: happyUpstreamAuthcode, - PKCECodeVerifier: oidcpkce.Code(happyDownstreamPKCE), - ExpectedIDTokenNonce: nonce.Nonce(happyDownstreamNonce), RedirectURI: happyUpstreamRedirectURI, + PKCECodeVerifier: oidcpkce.Code(happyDownstreamPKCEVerifier), + ExpectedIDTokenNonce: nonce.Nonce(happyDownstreamNonce), + } + + happyGitHubUpstreamExchangeAuthcodeArgs := &oidctestutil.ExchangeAuthcodeArgs{ + Authcode: happyUpstreamAuthcode, + RedirectURI: happyUpstreamRedirectURI, } // Note that fosite puts the granted scopes as a param in the redirect URI even though the spec doesn't seem to require it @@ -204,16 +242,16 @@ func TestCallbackEndpoint(t *testing.T) { wantDownstreamPKCEChallenge string wantDownstreamPKCEChallengeMethod string wantDownstreamCustomSessionData *psession.CustomSessionData - wantDownstreamAdditionalClaims map[string]any - - wantAuthcodeExchangeCall *expectedAuthcodeExchange + wantDownstreamAdditionalClaims map[string]interface{} + wantOIDCAuthcodeExchangeCall *expectedOIDCAuthcodeExchange + wantGitHubAuthcodeExchangeCall *expectedGitHubAuthcodeExchange }{ { - name: "GET with good state and cookie and successful upstream token exchange with response_mode=form_post returns 200 with HTML+JS form", - idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC(happyUpstream().Build()), + name: "OIDC: GET with good state and cookie and successful upstream token exchange with response_mode=form_post returns 200 with HTML+JS form", + idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC(happyOIDCUpstream().Build()), method: http.MethodGet, path: newRequestPath().WithState( - happyUpstreamStateParam().WithAuthorizeRequestParams( + happyOIDCUpstreamStateParam().WithAuthorizeRequestParams( shallowCopyAndModifyQuery( happyDownstreamRequestParamsQuery, map[string]string{"response_mode": "form_post"}, @@ -224,7 +262,7 @@ func TestCallbackEndpoint(t *testing.T) { wantStatus: http.StatusOK, wantContentType: "text/html;charset=UTF-8", wantBodyFormResponseRegexp: `(.+)`, - wantDownstreamIDTokenSubject: oidcUpstreamIssuer + "?idpName=" + happyUpstreamIDPName + "&sub=" + oidcUpstreamSubjectQueryEscaped, + wantDownstreamIDTokenSubject: oidcUpstreamIssuer + "?idpName=" + happyOIDCUpstreamIDPName + "&sub=" + oidcUpstreamSubjectQueryEscaped, wantDownstreamIDTokenUsername: oidcUpstreamUsername, wantDownstreamIDTokenGroups: oidcUpstreamGroupMembership, wantDownstreamRequestedScopes: happyDownstreamScopesRequested, @@ -233,15 +271,46 @@ func TestCallbackEndpoint(t *testing.T) { wantDownstreamClientID: downstreamPinnipedClientID, wantDownstreamPKCEChallenge: downstreamPKCEChallenge, wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod, - wantDownstreamCustomSessionData: happyDownstreamCustomSessionData, - wantAuthcodeExchangeCall: &expectedAuthcodeExchange{ - performedByUpstreamName: happyUpstreamIDPName, - args: happyExchangeAndValidateTokensArgs, + wantDownstreamCustomSessionData: happyDownstreamCustomSessionDataForOIDCUpstream, + wantOIDCAuthcodeExchangeCall: &expectedOIDCAuthcodeExchange{ + performedByUpstreamName: happyOIDCUpstreamIDPName, + args: happyOIDCUpstreamExchangeAuthcodeAndValidateTokenArgs, + }, + }, + { + name: "GitHub: GET with good state and cookie and successful upstream token exchange with response_mode=form_post returns 200 with HTML+JS form", + idps: testidplister.NewUpstreamIDPListerBuilder().WithGitHub(happyGitHubUpstream().Build()), + method: http.MethodGet, + path: newRequestPath().WithState( + happyGitHubUpstreamStateParam().WithAuthorizeRequestParams( + shallowCopyAndModifyQuery( + happyDownstreamRequestParamsQuery, + map[string]string{"response_mode": "form_post"}, + ).Encode(), + ).Build(t, happyStateCodec), + ).String(), + csrfCookie: happyCSRFCookie, + wantStatus: http.StatusOK, + wantContentType: "text/html;charset=UTF-8", + wantBodyFormResponseRegexp: `(.+)`, + wantDownstreamIDTokenSubject: githubDownstreamSubject, + wantDownstreamIDTokenUsername: githubUpstreamUsername, + wantDownstreamIDTokenGroups: githubUpstreamGroupMembership, + wantDownstreamRequestedScopes: happyDownstreamScopesRequested, + wantDownstreamGrantedScopes: happyDownstreamScopesGranted, + wantDownstreamNonce: downstreamNonce, + wantDownstreamClientID: downstreamPinnipedClientID, + wantDownstreamPKCEChallenge: downstreamPKCEChallenge, + wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod, + wantDownstreamCustomSessionData: happyDownstreamCustomSessionDataForGitHubUpstream, + wantGitHubAuthcodeExchangeCall: &expectedGitHubAuthcodeExchange{ + performedByUpstreamName: happyGithubIDPName, + args: happyGitHubUpstreamExchangeAuthcodeArgs, }, }, { name: "GET with good state and cookie with additional params", - idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC(happyUpstream(). + idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC(happyOIDCUpstream(). WithAdditionalClaimMappings(map[string]string{ "downstreamCustomClaim": "upstreamCustomClaim", "downstreamOtherClaim": "upstreamOtherClaim", @@ -252,7 +321,7 @@ func TestCallbackEndpoint(t *testing.T) { Build()), method: http.MethodGet, path: newRequestPath().WithState( - happyUpstreamStateParam().WithAuthorizeRequestParams( + happyOIDCUpstreamStateParam().WithAuthorizeRequestParams( shallowCopyAndModifyQuery( happyDownstreamRequestParamsQuery, map[string]string{"response_mode": "form_post"}, @@ -263,7 +332,7 @@ func TestCallbackEndpoint(t *testing.T) { wantStatus: http.StatusOK, wantContentType: "text/html;charset=UTF-8", wantBodyFormResponseRegexp: `(.+)`, - wantDownstreamIDTokenSubject: oidcUpstreamIssuer + "?idpName=" + happyUpstreamIDPName + "&sub=" + oidcUpstreamSubjectQueryEscaped, + wantDownstreamIDTokenSubject: oidcUpstreamIssuer + "?idpName=" + happyOIDCUpstreamIDPName + "&sub=" + oidcUpstreamSubjectQueryEscaped, wantDownstreamIDTokenUsername: oidcUpstreamUsername, wantDownstreamIDTokenGroups: oidcUpstreamGroupMembership, wantDownstreamRequestedScopes: happyDownstreamScopesRequested, @@ -272,10 +341,10 @@ func TestCallbackEndpoint(t *testing.T) { wantDownstreamClientID: downstreamPinnipedClientID, wantDownstreamPKCEChallenge: downstreamPKCEChallenge, wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod, - wantDownstreamCustomSessionData: happyDownstreamCustomSessionData, - wantAuthcodeExchangeCall: &expectedAuthcodeExchange{ - performedByUpstreamName: happyUpstreamIDPName, - args: happyExchangeAndValidateTokensArgs, + wantDownstreamCustomSessionData: happyDownstreamCustomSessionDataForOIDCUpstream, + wantOIDCAuthcodeExchangeCall: &expectedOIDCAuthcodeExchange{ + performedByUpstreamName: happyOIDCUpstreamIDPName, + args: happyOIDCUpstreamExchangeAuthcodeAndValidateTokenArgs, }, wantDownstreamAdditionalClaims: map[string]any{ "downstreamCustomClaim": "i am a claim value", @@ -284,14 +353,14 @@ func TestCallbackEndpoint(t *testing.T) { }, { name: "GET with good state and cookie and successful upstream token exchange returns 303 to downstream client callback with its state and code", - idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC(happyUpstream().Build()), + idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC(happyOIDCUpstream().Build()), method: http.MethodGet, - path: newRequestPath().WithState(happyState).String(), + path: newRequestPath().WithState(happyOIDCState).String(), csrfCookie: happyCSRFCookie, wantStatus: http.StatusSeeOther, wantRedirectLocationRegexp: happyDownstreamRedirectLocationRegexp, wantBody: "", - wantDownstreamIDTokenSubject: oidcUpstreamIssuer + "?idpName=" + happyUpstreamIDPName + "&sub=" + oidcUpstreamSubjectQueryEscaped, + wantDownstreamIDTokenSubject: oidcUpstreamIssuer + "?idpName=" + happyOIDCUpstreamIDPName + "&sub=" + oidcUpstreamSubjectQueryEscaped, wantDownstreamIDTokenUsername: oidcUpstreamUsername, wantDownstreamIDTokenGroups: oidcUpstreamGroupMembership, wantDownstreamRequestedScopes: happyDownstreamScopesRequested, @@ -300,23 +369,23 @@ func TestCallbackEndpoint(t *testing.T) { wantDownstreamClientID: downstreamPinnipedClientID, wantDownstreamPKCEChallenge: downstreamPKCEChallenge, wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod, - wantDownstreamCustomSessionData: happyDownstreamCustomSessionData, - wantAuthcodeExchangeCall: &expectedAuthcodeExchange{ - performedByUpstreamName: happyUpstreamIDPName, - args: happyExchangeAndValidateTokensArgs, + wantDownstreamCustomSessionData: happyDownstreamCustomSessionDataForOIDCUpstream, + wantOIDCAuthcodeExchangeCall: &expectedOIDCAuthcodeExchange{ + performedByUpstreamName: happyOIDCUpstreamIDPName, + args: happyOIDCUpstreamExchangeAuthcodeAndValidateTokenArgs, }, }, { name: "GET with good state and cookie and successful upstream token exchange returns 303 to downstream client callback with its state and code when using dynamic client", - idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC(happyUpstream().Build()), + idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC(happyOIDCUpstream().Build()), kubeResources: addFullyCapableDynamicClientAndSecretToKubeResources, method: http.MethodGet, - path: newRequestPath().WithState(happyStateForDynamicClient).String(), + path: newRequestPath().WithState(happyOIDCStateForDynamicClient).String(), csrfCookie: happyCSRFCookie, wantStatus: http.StatusSeeOther, wantRedirectLocationRegexp: happyDownstreamRedirectLocationRegexp, wantBody: "", - wantDownstreamIDTokenSubject: oidcUpstreamIssuer + "?idpName=" + happyUpstreamIDPName + "&sub=" + oidcUpstreamSubjectQueryEscaped, + wantDownstreamIDTokenSubject: oidcUpstreamIssuer + "?idpName=" + happyOIDCUpstreamIDPName + "&sub=" + oidcUpstreamSubjectQueryEscaped, wantDownstreamIDTokenUsername: oidcUpstreamUsername, wantDownstreamIDTokenGroups: oidcUpstreamGroupMembership, wantDownstreamRequestedScopes: happyDownstreamScopesRequested, @@ -325,22 +394,22 @@ func TestCallbackEndpoint(t *testing.T) { wantDownstreamClientID: downstreamDynamicClientID, wantDownstreamPKCEChallenge: downstreamPKCEChallenge, wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod, - wantDownstreamCustomSessionData: happyDownstreamCustomSessionData, - wantAuthcodeExchangeCall: &expectedAuthcodeExchange{ - performedByUpstreamName: happyUpstreamIDPName, - args: happyExchangeAndValidateTokensArgs, + wantDownstreamCustomSessionData: happyDownstreamCustomSessionDataForOIDCUpstream, + wantOIDCAuthcodeExchangeCall: &expectedOIDCAuthcodeExchange{ + performedByUpstreamName: happyOIDCUpstreamIDPName, + args: happyOIDCUpstreamExchangeAuthcodeAndValidateTokenArgs, }, }, { name: "GET with authcode exchange that returns an access token but no refresh token when there is a userinfo endpoint returns 303 to downstream client callback with its state and code", - idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC(happyUpstream().WithEmptyRefreshToken().WithAccessToken(oidcUpstreamAccessToken, metav1.NewTime(time.Now().Add(9*time.Hour))).WithUserInfoURL().Build()), + idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC(happyOIDCUpstream().WithEmptyRefreshToken().WithAccessToken(oidcUpstreamAccessToken, metav1.NewTime(time.Now().Add(9*time.Hour))).WithUserInfoURL().Build()), method: http.MethodGet, - path: newRequestPath().WithState(happyState).String(), + path: newRequestPath().WithState(happyOIDCState).String(), csrfCookie: happyCSRFCookie, wantStatus: http.StatusSeeOther, wantRedirectLocationRegexp: happyDownstreamRedirectLocationRegexp, wantBody: "", - wantDownstreamIDTokenSubject: oidcUpstreamIssuer + "?idpName=" + happyUpstreamIDPName + "&sub=" + oidcUpstreamSubjectQueryEscaped, + wantDownstreamIDTokenSubject: oidcUpstreamIssuer + "?idpName=" + happyOIDCUpstreamIDPName + "&sub=" + oidcUpstreamSubjectQueryEscaped, wantDownstreamIDTokenUsername: oidcUpstreamUsername, wantDownstreamIDTokenGroups: oidcUpstreamGroupMembership, wantDownstreamRequestedScopes: happyDownstreamScopesRequested, @@ -350,17 +419,17 @@ func TestCallbackEndpoint(t *testing.T) { wantDownstreamPKCEChallenge: downstreamPKCEChallenge, wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod, wantDownstreamCustomSessionData: happyDownstreamAccessTokenCustomSessionData, - wantAuthcodeExchangeCall: &expectedAuthcodeExchange{ - performedByUpstreamName: happyUpstreamIDPName, - args: happyExchangeAndValidateTokensArgs, + wantOIDCAuthcodeExchangeCall: &expectedOIDCAuthcodeExchange{ + performedByUpstreamName: happyOIDCUpstreamIDPName, + args: happyOIDCUpstreamExchangeAuthcodeAndValidateTokenArgs, }, }, { name: "form_post happy path without username or groups scopes requested", - idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC(happyUpstream().Build()), + idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC(happyOIDCUpstream().Build()), method: http.MethodGet, path: newRequestPath().WithState( - happyUpstreamStateParam().WithAuthorizeRequestParams( + happyOIDCUpstreamStateParam().WithAuthorizeRequestParams( shallowCopyAndModifyQuery( happyDownstreamRequestParamsQuery, map[string]string{ @@ -374,7 +443,7 @@ func TestCallbackEndpoint(t *testing.T) { wantStatus: http.StatusOK, wantContentType: "text/html;charset=UTF-8", wantBodyFormResponseRegexp: `(.+)`, - wantDownstreamIDTokenSubject: oidcUpstreamIssuer + "?idpName=" + happyUpstreamIDPName + "&sub=" + oidcUpstreamSubjectQueryEscaped, + wantDownstreamIDTokenSubject: oidcUpstreamIssuer + "?idpName=" + happyOIDCUpstreamIDPName + "&sub=" + oidcUpstreamSubjectQueryEscaped, wantDownstreamIDTokenUsername: oidcUpstreamUsername, wantDownstreamRequestedScopes: []string{"openid"}, wantDownstreamIDTokenGroups: oidcUpstreamGroupMembership, @@ -384,22 +453,22 @@ func TestCallbackEndpoint(t *testing.T) { wantDownstreamClientID: downstreamPinnipedClientID, wantDownstreamPKCEChallenge: downstreamPKCEChallenge, wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod, - wantDownstreamCustomSessionData: happyDownstreamCustomSessionData, - wantAuthcodeExchangeCall: &expectedAuthcodeExchange{ - performedByUpstreamName: happyUpstreamIDPName, - args: happyExchangeAndValidateTokensArgs, + wantDownstreamCustomSessionData: happyDownstreamCustomSessionDataForOIDCUpstream, + wantOIDCAuthcodeExchangeCall: &expectedOIDCAuthcodeExchange{ + performedByUpstreamName: happyOIDCUpstreamIDPName, + args: happyOIDCUpstreamExchangeAuthcodeAndValidateTokenArgs, }, }, { name: "GET with authcode exchange that returns an access token but no refresh token but has a short token lifetime which is stored as a warning in the session", - idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC(happyUpstream().WithEmptyRefreshToken().WithAccessToken(oidcUpstreamAccessToken, metav1.NewTime(time.Now().Add(1*time.Hour))).WithUserInfoURL().Build()), + idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC(happyOIDCUpstream().WithEmptyRefreshToken().WithAccessToken(oidcUpstreamAccessToken, metav1.NewTime(time.Now().Add(1*time.Hour))).WithUserInfoURL().Build()), method: http.MethodGet, - path: newRequestPath().WithState(happyState).String(), + path: newRequestPath().WithState(happyOIDCState).String(), csrfCookie: happyCSRFCookie, wantStatus: http.StatusSeeOther, wantRedirectLocationRegexp: happyDownstreamRedirectLocationRegexp, wantBody: "", - wantDownstreamIDTokenSubject: oidcUpstreamIssuer + "?idpName=" + happyUpstreamIDPName + "&sub=" + oidcUpstreamSubjectQueryEscaped, + wantDownstreamIDTokenSubject: oidcUpstreamIssuer + "?idpName=" + happyOIDCUpstreamIDPName + "&sub=" + oidcUpstreamSubjectQueryEscaped, wantDownstreamIDTokenUsername: oidcUpstreamUsername, wantDownstreamIDTokenGroups: oidcUpstreamGroupMembership, wantDownstreamRequestedScopes: happyDownstreamScopesRequested, @@ -412,8 +481,8 @@ func TestCallbackEndpoint(t *testing.T) { Username: oidcUpstreamUsername, UpstreamUsername: oidcUpstreamUsername, UpstreamGroups: oidcUpstreamGroupMembership, - ProviderUID: happyUpstreamIDPResourceUID, - ProviderName: happyUpstreamIDPName, + ProviderUID: happyOIDCUpstreamIDPResourceUID, + ProviderName: happyOIDCUpstreamIDPName, ProviderType: psession.ProviderTypeOIDC, Warnings: []string{"Access token from identity provider has lifetime of less than 3 hours. Expect frequent prompts to log in."}, OIDC: &psession.OIDCSessionData{ @@ -422,23 +491,23 @@ func TestCallbackEndpoint(t *testing.T) { UpstreamSubject: oidcUpstreamSubject, }, }, - wantAuthcodeExchangeCall: &expectedAuthcodeExchange{ - performedByUpstreamName: happyUpstreamIDPName, - args: happyExchangeAndValidateTokensArgs, + wantOIDCAuthcodeExchangeCall: &expectedOIDCAuthcodeExchange{ + performedByUpstreamName: happyOIDCUpstreamIDPName, + args: happyOIDCUpstreamExchangeAuthcodeAndValidateTokenArgs, }, }, { name: "upstream IDP provides no username or group claim configuration, so we use default username claim and skip groups", idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC( - happyUpstream().WithoutUsernameClaim().WithoutGroupsClaim().Build(), + happyOIDCUpstream().WithoutUsernameClaim().WithoutGroupsClaim().Build(), ), method: http.MethodGet, - path: newRequestPath().WithState(happyState).String(), + path: newRequestPath().WithState(happyOIDCState).String(), csrfCookie: happyCSRFCookie, wantStatus: http.StatusSeeOther, wantRedirectLocationRegexp: happyDownstreamRedirectLocationRegexp, wantBody: "", - wantDownstreamIDTokenSubject: oidcUpstreamIssuer + "?idpName=" + happyUpstreamIDPName + "&sub=" + oidcUpstreamSubjectQueryEscaped, + wantDownstreamIDTokenSubject: oidcUpstreamIssuer + "?idpName=" + happyOIDCUpstreamIDPName + "&sub=" + oidcUpstreamSubjectQueryEscaped, wantDownstreamIDTokenUsername: oidcUpstreamIssuer + "?sub=" + oidcUpstreamSubjectQueryEscaped, wantDownstreamIDTokenGroups: []string{}, wantDownstreamRequestedScopes: happyDownstreamScopesRequested, @@ -448,27 +517,28 @@ func TestCallbackEndpoint(t *testing.T) { wantDownstreamPKCEChallenge: downstreamPKCEChallenge, wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod, wantDownstreamCustomSessionData: happyDownstreamCustomSessionDataWithUsernameAndGroups( + happyDownstreamCustomSessionDataForOIDCUpstream, oidcUpstreamIssuer+"?sub="+oidcUpstreamSubjectQueryEscaped, oidcUpstreamIssuer+"?sub="+oidcUpstreamSubjectQueryEscaped, nil, ), - wantAuthcodeExchangeCall: &expectedAuthcodeExchange{ - performedByUpstreamName: happyUpstreamIDPName, - args: happyExchangeAndValidateTokensArgs, + wantOIDCAuthcodeExchangeCall: &expectedOIDCAuthcodeExchange{ + performedByUpstreamName: happyOIDCUpstreamIDPName, + args: happyOIDCUpstreamExchangeAuthcodeAndValidateTokenArgs, }, }, { name: "upstream IDP configures username claim as special claim `email` and `email_verified` upstream claim is missing", idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC( - happyUpstream().WithUsernameClaim("email").WithIDTokenClaim("email", "joe@whitehouse.gov").Build(), + happyOIDCUpstream().WithUsernameClaim("email").WithIDTokenClaim("email", "joe@whitehouse.gov").Build(), ), method: http.MethodGet, - path: newRequestPath().WithState(happyState).String(), + path: newRequestPath().WithState(happyOIDCState).String(), csrfCookie: happyCSRFCookie, wantStatus: http.StatusSeeOther, wantRedirectLocationRegexp: happyDownstreamRedirectLocationRegexp, wantBody: "", - wantDownstreamIDTokenSubject: oidcUpstreamIssuer + "?idpName=" + happyUpstreamIDPName + "&sub=" + oidcUpstreamSubjectQueryEscaped, + wantDownstreamIDTokenSubject: oidcUpstreamIssuer + "?idpName=" + happyOIDCUpstreamIDPName + "&sub=" + oidcUpstreamSubjectQueryEscaped, wantDownstreamIDTokenUsername: "joe@whitehouse.gov", wantDownstreamIDTokenGroups: oidcUpstreamGroupMembership, wantDownstreamRequestedScopes: happyDownstreamScopesRequested, @@ -478,29 +548,30 @@ func TestCallbackEndpoint(t *testing.T) { wantDownstreamPKCEChallenge: downstreamPKCEChallenge, wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod, wantDownstreamCustomSessionData: happyDownstreamCustomSessionDataWithUsernameAndGroups( + happyDownstreamCustomSessionDataForOIDCUpstream, "joe@whitehouse.gov", "joe@whitehouse.gov", oidcUpstreamGroupMembership, ), - wantAuthcodeExchangeCall: &expectedAuthcodeExchange{ - performedByUpstreamName: happyUpstreamIDPName, - args: happyExchangeAndValidateTokensArgs, + wantOIDCAuthcodeExchangeCall: &expectedOIDCAuthcodeExchange{ + performedByUpstreamName: happyOIDCUpstreamIDPName, + args: happyOIDCUpstreamExchangeAuthcodeAndValidateTokenArgs, }, }, { name: "upstream IDP configures username claim as special claim `email` and `email_verified` upstream claim is present with true value", idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC( - happyUpstream().WithUsernameClaim("email"). + happyOIDCUpstream().WithUsernameClaim("email"). WithIDTokenClaim("email", "joe@whitehouse.gov"). WithIDTokenClaim("email_verified", true).Build(), ), method: http.MethodGet, - path: newRequestPath().WithState(happyState).String(), + path: newRequestPath().WithState(happyOIDCState).String(), csrfCookie: happyCSRFCookie, wantStatus: http.StatusSeeOther, wantRedirectLocationRegexp: happyDownstreamRedirectLocationRegexp, wantBody: "", - wantDownstreamIDTokenSubject: oidcUpstreamIssuer + "?idpName=" + happyUpstreamIDPName + "&sub=" + oidcUpstreamSubjectQueryEscaped, + wantDownstreamIDTokenSubject: oidcUpstreamIssuer + "?idpName=" + happyOIDCUpstreamIDPName + "&sub=" + oidcUpstreamSubjectQueryEscaped, wantDownstreamIDTokenUsername: "joe@whitehouse.gov", wantDownstreamIDTokenGroups: oidcUpstreamGroupMembership, wantDownstreamRequestedScopes: happyDownstreamScopesRequested, @@ -510,30 +581,31 @@ func TestCallbackEndpoint(t *testing.T) { wantDownstreamPKCEChallenge: downstreamPKCEChallenge, wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod, wantDownstreamCustomSessionData: happyDownstreamCustomSessionDataWithUsernameAndGroups( + happyDownstreamCustomSessionDataForOIDCUpstream, "joe@whitehouse.gov", "joe@whitehouse.gov", oidcUpstreamGroupMembership, ), - wantAuthcodeExchangeCall: &expectedAuthcodeExchange{ - performedByUpstreamName: happyUpstreamIDPName, - args: happyExchangeAndValidateTokensArgs, + wantOIDCAuthcodeExchangeCall: &expectedOIDCAuthcodeExchange{ + performedByUpstreamName: happyOIDCUpstreamIDPName, + args: happyOIDCUpstreamExchangeAuthcodeAndValidateTokenArgs, }, }, { name: "upstream IDP configures username claim as anything other than special claim `email` and `email_verified` upstream claim is present with false value", idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC( - happyUpstream().WithUsernameClaim("some-claim"). + happyOIDCUpstream().WithUsernameClaim("some-claim"). WithIDTokenClaim("some-claim", "joe"). WithIDTokenClaim("email", "joe@whitehouse.gov"). WithIDTokenClaim("email_verified", false).Build(), ), method: http.MethodGet, - path: newRequestPath().WithState(happyState).String(), + path: newRequestPath().WithState(happyOIDCState).String(), csrfCookie: happyCSRFCookie, wantStatus: http.StatusSeeOther, // succeed despite `email_verified=false` because we're not using the email claim for anything wantRedirectLocationRegexp: happyDownstreamRedirectLocationRegexp, wantBody: "", - wantDownstreamIDTokenSubject: oidcUpstreamIssuer + "?idpName=" + happyUpstreamIDPName + "&sub=" + oidcUpstreamSubjectQueryEscaped, + wantDownstreamIDTokenSubject: oidcUpstreamIssuer + "?idpName=" + happyOIDCUpstreamIDPName + "&sub=" + oidcUpstreamSubjectQueryEscaped, wantDownstreamIDTokenUsername: "joe", wantDownstreamIDTokenGroups: oidcUpstreamGroupMembership, wantDownstreamRequestedScopes: happyDownstreamScopesRequested, @@ -543,132 +615,133 @@ func TestCallbackEndpoint(t *testing.T) { wantDownstreamPKCEChallenge: downstreamPKCEChallenge, wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod, wantDownstreamCustomSessionData: happyDownstreamCustomSessionDataWithUsernameAndGroups( + happyDownstreamCustomSessionDataForOIDCUpstream, "joe", "joe", oidcUpstreamGroupMembership, ), - wantAuthcodeExchangeCall: &expectedAuthcodeExchange{ - performedByUpstreamName: happyUpstreamIDPName, - args: happyExchangeAndValidateTokensArgs, + wantOIDCAuthcodeExchangeCall: &expectedOIDCAuthcodeExchange{ + performedByUpstreamName: happyOIDCUpstreamIDPName, + args: happyOIDCUpstreamExchangeAuthcodeAndValidateTokenArgs, }, }, { name: "upstream IDP configures username claim as special claim `email` and `email_verified` upstream claim is present with illegal value", - idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC(happyUpstream().WithUsernameClaim("email"). + idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC(happyOIDCUpstream().WithUsernameClaim("email"). WithIDTokenClaim("email", "joe@whitehouse.gov"). WithIDTokenClaim("email_verified", "supposed to be boolean").Build(), ), method: http.MethodGet, - path: newRequestPath().WithState(happyState).String(), + path: newRequestPath().WithState(happyOIDCState).String(), csrfCookie: happyCSRFCookie, wantStatus: http.StatusUnprocessableEntity, wantContentType: htmlContentType, wantBody: "Unprocessable Entity: email_verified claim in upstream ID token has invalid format\n", - wantAuthcodeExchangeCall: &expectedAuthcodeExchange{ - performedByUpstreamName: happyUpstreamIDPName, - args: happyExchangeAndValidateTokensArgs, + wantOIDCAuthcodeExchangeCall: &expectedOIDCAuthcodeExchange{ + performedByUpstreamName: happyOIDCUpstreamIDPName, + args: happyOIDCUpstreamExchangeAuthcodeAndValidateTokenArgs, }, }, { name: "return an error when upstream IDP returned no refresh token with an access token when there is no userinfo endpoint", - idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC(happyUpstream().WithoutRefreshToken().WithAccessToken(oidcUpstreamAccessToken, metav1.NewTime(time.Now().Add(9*time.Hour))).WithoutUserInfoURL().Build()), + idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC(happyOIDCUpstream().WithoutRefreshToken().WithAccessToken(oidcUpstreamAccessToken, metav1.NewTime(time.Now().Add(9*time.Hour))).WithoutUserInfoURL().Build()), method: http.MethodGet, - path: newRequestPath().WithState(happyState).String(), + path: newRequestPath().WithState(happyOIDCState).String(), csrfCookie: happyCSRFCookie, wantStatus: http.StatusUnprocessableEntity, wantContentType: htmlContentType, wantBody: "Unprocessable Entity: access token was returned by upstream provider but there was no userinfo endpoint\n", - wantAuthcodeExchangeCall: &expectedAuthcodeExchange{ - performedByUpstreamName: happyUpstreamIDPName, - args: happyExchangeAndValidateTokensArgs, + wantOIDCAuthcodeExchangeCall: &expectedOIDCAuthcodeExchange{ + performedByUpstreamName: happyOIDCUpstreamIDPName, + args: happyOIDCUpstreamExchangeAuthcodeAndValidateTokenArgs, }, }, { name: "return an error when upstream IDP returned no refresh token and no access token", - idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC(happyUpstream().WithoutRefreshToken().WithoutAccessToken().Build()), + idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC(happyOIDCUpstream().WithoutRefreshToken().WithoutAccessToken().Build()), method: http.MethodGet, - path: newRequestPath().WithState(happyState).String(), + path: newRequestPath().WithState(happyOIDCState).String(), csrfCookie: happyCSRFCookie, wantStatus: http.StatusUnprocessableEntity, wantContentType: htmlContentType, wantBody: "Unprocessable Entity: neither access token nor refresh token returned by upstream provider\n", - wantAuthcodeExchangeCall: &expectedAuthcodeExchange{ - performedByUpstreamName: happyUpstreamIDPName, - args: happyExchangeAndValidateTokensArgs, + wantOIDCAuthcodeExchangeCall: &expectedOIDCAuthcodeExchange{ + performedByUpstreamName: happyOIDCUpstreamIDPName, + args: happyOIDCUpstreamExchangeAuthcodeAndValidateTokenArgs, }, }, { name: "return an error when upstream IDP returned an empty refresh token and empty access token", - idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC(happyUpstream().WithEmptyRefreshToken().WithEmptyAccessToken().Build()), + idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC(happyOIDCUpstream().WithEmptyRefreshToken().WithEmptyAccessToken().Build()), method: http.MethodGet, - path: newRequestPath().WithState(happyState).String(), + path: newRequestPath().WithState(happyOIDCState).String(), csrfCookie: happyCSRFCookie, wantStatus: http.StatusUnprocessableEntity, wantContentType: htmlContentType, wantBody: "Unprocessable Entity: neither access token nor refresh token returned by upstream provider\n", - wantAuthcodeExchangeCall: &expectedAuthcodeExchange{ - performedByUpstreamName: happyUpstreamIDPName, - args: happyExchangeAndValidateTokensArgs, + wantOIDCAuthcodeExchangeCall: &expectedOIDCAuthcodeExchange{ + performedByUpstreamName: happyOIDCUpstreamIDPName, + args: happyOIDCUpstreamExchangeAuthcodeAndValidateTokenArgs, }, }, { name: "return an error when upstream IDP returned no refresh token and empty access token", - idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC(happyUpstream().WithoutRefreshToken().WithEmptyAccessToken().Build()), + idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC(happyOIDCUpstream().WithoutRefreshToken().WithEmptyAccessToken().Build()), method: http.MethodGet, - path: newRequestPath().WithState(happyState).String(), + path: newRequestPath().WithState(happyOIDCState).String(), csrfCookie: happyCSRFCookie, wantStatus: http.StatusUnprocessableEntity, wantContentType: htmlContentType, wantBody: "Unprocessable Entity: neither access token nor refresh token returned by upstream provider\n", - wantAuthcodeExchangeCall: &expectedAuthcodeExchange{ - performedByUpstreamName: happyUpstreamIDPName, - args: happyExchangeAndValidateTokensArgs, + wantOIDCAuthcodeExchangeCall: &expectedOIDCAuthcodeExchange{ + performedByUpstreamName: happyOIDCUpstreamIDPName, + args: happyOIDCUpstreamExchangeAuthcodeAndValidateTokenArgs, }, }, { name: "return an error when upstream IDP returned an empty refresh token and no access token", - idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC(happyUpstream().WithEmptyRefreshToken().WithoutAccessToken().Build()), + idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC(happyOIDCUpstream().WithEmptyRefreshToken().WithoutAccessToken().Build()), method: http.MethodGet, - path: newRequestPath().WithState(happyState).String(), + path: newRequestPath().WithState(happyOIDCState).String(), csrfCookie: happyCSRFCookie, wantStatus: http.StatusUnprocessableEntity, wantContentType: htmlContentType, wantBody: "Unprocessable Entity: neither access token nor refresh token returned by upstream provider\n", - wantAuthcodeExchangeCall: &expectedAuthcodeExchange{ - performedByUpstreamName: happyUpstreamIDPName, - args: happyExchangeAndValidateTokensArgs, + wantOIDCAuthcodeExchangeCall: &expectedOIDCAuthcodeExchange{ + performedByUpstreamName: happyOIDCUpstreamIDPName, + args: happyOIDCUpstreamExchangeAuthcodeAndValidateTokenArgs, }, }, { name: "upstream IDP configures username claim as special claim `email` and `email_verified` upstream claim is present with false value", idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC( - happyUpstream().WithUsernameClaim("email"). + happyOIDCUpstream().WithUsernameClaim("email"). WithIDTokenClaim("email", "joe@whitehouse.gov"). WithIDTokenClaim("email_verified", false).Build(), ), method: http.MethodGet, - path: newRequestPath().WithState(happyState).String(), + path: newRequestPath().WithState(happyOIDCState).String(), csrfCookie: happyCSRFCookie, wantStatus: http.StatusUnprocessableEntity, wantContentType: htmlContentType, wantBody: "Unprocessable Entity: email_verified claim in upstream ID token has false value\n", - wantAuthcodeExchangeCall: &expectedAuthcodeExchange{ - performedByUpstreamName: happyUpstreamIDPName, - args: happyExchangeAndValidateTokensArgs, + wantOIDCAuthcodeExchangeCall: &expectedOIDCAuthcodeExchange{ + performedByUpstreamName: happyOIDCUpstreamIDPName, + args: happyOIDCUpstreamExchangeAuthcodeAndValidateTokenArgs, }, }, { name: "upstream IDP provides username claim configuration as `sub`, so the downstream token subject should be exactly what they asked for", idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC( - happyUpstream().WithUsernameClaim("sub").Build(), + happyOIDCUpstream().WithUsernameClaim("sub").Build(), ), method: http.MethodGet, - path: newRequestPath().WithState(happyState).String(), + path: newRequestPath().WithState(happyOIDCState).String(), csrfCookie: happyCSRFCookie, wantStatus: http.StatusSeeOther, wantRedirectLocationRegexp: happyDownstreamRedirectLocationRegexp, wantBody: "", - wantDownstreamIDTokenSubject: oidcUpstreamIssuer + "?idpName=" + happyUpstreamIDPName + "&sub=" + oidcUpstreamSubjectQueryEscaped, + wantDownstreamIDTokenSubject: oidcUpstreamIssuer + "?idpName=" + happyOIDCUpstreamIDPName + "&sub=" + oidcUpstreamSubjectQueryEscaped, wantDownstreamIDTokenUsername: oidcUpstreamSubject, wantDownstreamIDTokenGroups: oidcUpstreamGroupMembership, wantDownstreamRequestedScopes: happyDownstreamScopesRequested, @@ -678,27 +751,28 @@ func TestCallbackEndpoint(t *testing.T) { wantDownstreamPKCEChallenge: downstreamPKCEChallenge, wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod, wantDownstreamCustomSessionData: happyDownstreamCustomSessionDataWithUsernameAndGroups( + happyDownstreamCustomSessionDataForOIDCUpstream, oidcUpstreamSubject, oidcUpstreamSubject, oidcUpstreamGroupMembership, ), - wantAuthcodeExchangeCall: &expectedAuthcodeExchange{ - performedByUpstreamName: happyUpstreamIDPName, - args: happyExchangeAndValidateTokensArgs, + wantOIDCAuthcodeExchangeCall: &expectedOIDCAuthcodeExchange{ + performedByUpstreamName: happyOIDCUpstreamIDPName, + args: happyOIDCUpstreamExchangeAuthcodeAndValidateTokenArgs, }, }, { name: "upstream IDP's configured groups claim in the ID token has a non-array value", idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC( - happyUpstream().WithIDTokenClaim(oidcUpstreamGroupsClaim, "notAnArrayGroup1 notAnArrayGroup2").Build(), + happyOIDCUpstream().WithIDTokenClaim(oidcUpstreamGroupsClaim, "notAnArrayGroup1 notAnArrayGroup2").Build(), ), method: http.MethodGet, - path: newRequestPath().WithState(happyState).String(), + path: newRequestPath().WithState(happyOIDCState).String(), csrfCookie: happyCSRFCookie, wantStatus: http.StatusSeeOther, wantRedirectLocationRegexp: happyDownstreamRedirectLocationRegexp, wantBody: "", - wantDownstreamIDTokenSubject: oidcUpstreamIssuer + "?idpName=" + happyUpstreamIDPName + "&sub=" + oidcUpstreamSubjectQueryEscaped, + wantDownstreamIDTokenSubject: oidcUpstreamIssuer + "?idpName=" + happyOIDCUpstreamIDPName + "&sub=" + oidcUpstreamSubjectQueryEscaped, wantDownstreamIDTokenUsername: oidcUpstreamUsername, wantDownstreamIDTokenGroups: []string{"notAnArrayGroup1 notAnArrayGroup2"}, wantDownstreamRequestedScopes: happyDownstreamScopesRequested, @@ -708,27 +782,28 @@ func TestCallbackEndpoint(t *testing.T) { wantDownstreamPKCEChallenge: downstreamPKCEChallenge, wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod, wantDownstreamCustomSessionData: happyDownstreamCustomSessionDataWithUsernameAndGroups( + happyDownstreamCustomSessionDataForOIDCUpstream, oidcUpstreamUsername, oidcUpstreamUsername, []string{"notAnArrayGroup1 notAnArrayGroup2"}, ), - wantAuthcodeExchangeCall: &expectedAuthcodeExchange{ - performedByUpstreamName: happyUpstreamIDPName, - args: happyExchangeAndValidateTokensArgs, + wantOIDCAuthcodeExchangeCall: &expectedOIDCAuthcodeExchange{ + performedByUpstreamName: happyOIDCUpstreamIDPName, + args: happyOIDCUpstreamExchangeAuthcodeAndValidateTokenArgs, }, }, { name: "upstream IDP's configured groups claim in the ID token is a slice of interfaces", idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC( - happyUpstream().WithIDTokenClaim(oidcUpstreamGroupsClaim, []any{"group1", "group2"}).Build(), + happyOIDCUpstream().WithIDTokenClaim(oidcUpstreamGroupsClaim, []interface{}{"group1", "group2"}).Build(), ), method: http.MethodGet, - path: newRequestPath().WithState(happyState).String(), + path: newRequestPath().WithState(happyOIDCState).String(), csrfCookie: happyCSRFCookie, wantStatus: http.StatusSeeOther, wantRedirectLocationRegexp: happyDownstreamRedirectLocationRegexp, wantBody: "", - wantDownstreamIDTokenSubject: oidcUpstreamIssuer + "?idpName=" + happyUpstreamIDPName + "&sub=" + oidcUpstreamSubjectQueryEscaped, + wantDownstreamIDTokenSubject: oidcUpstreamIssuer + "?idpName=" + happyOIDCUpstreamIDPName + "&sub=" + oidcUpstreamSubjectQueryEscaped, wantDownstreamIDTokenUsername: oidcUpstreamUsername, wantDownstreamIDTokenGroups: []string{"group1", "group2"}, wantDownstreamRequestedScopes: happyDownstreamScopesRequested, @@ -738,22 +813,23 @@ func TestCallbackEndpoint(t *testing.T) { wantDownstreamPKCEChallenge: downstreamPKCEChallenge, wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod, wantDownstreamCustomSessionData: happyDownstreamCustomSessionDataWithUsernameAndGroups( + happyDownstreamCustomSessionDataForOIDCUpstream, oidcUpstreamUsername, oidcUpstreamUsername, []string{"group1", "group2"}, ), - wantAuthcodeExchangeCall: &expectedAuthcodeExchange{ - performedByUpstreamName: happyUpstreamIDPName, - args: happyExchangeAndValidateTokensArgs, + wantOIDCAuthcodeExchangeCall: &expectedOIDCAuthcodeExchange{ + performedByUpstreamName: happyOIDCUpstreamIDPName, + args: happyOIDCUpstreamExchangeAuthcodeAndValidateTokenArgs, }, }, { name: "using dynamic client which is allowed to request username scope, but does not actually request username scope in authorize request, does not get username in ID token", - idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC(happyUpstream().Build()), + idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC(happyOIDCUpstream().Build()), kubeResources: addFullyCapableDynamicClientAndSecretToKubeResources, method: http.MethodGet, path: newRequestPath().WithState( - happyUpstreamStateParamForDynamicClient(). + happyOIDCUpstreamStateParamForDynamicClient(). WithAuthorizeRequestParams(shallowCopyAndModifyQuery(happyDownstreamRequestParamsQueryForDynamicClient, map[string]string{"scope": "openid groups offline_access"}).Encode()). Build(t, happyStateCodec), @@ -762,7 +838,7 @@ func TestCallbackEndpoint(t *testing.T) { wantStatus: http.StatusSeeOther, wantRedirectLocationRegexp: downstreamRedirectURI + `\?code=([^&]+)&scope=openid\+offline_access\+groups&state=` + happyDownstreamState, wantBody: "", - wantDownstreamIDTokenSubject: oidcUpstreamIssuer + "?idpName=" + happyUpstreamIDPName + "&sub=" + oidcUpstreamSubjectQueryEscaped, + wantDownstreamIDTokenSubject: oidcUpstreamIssuer + "?idpName=" + happyOIDCUpstreamIDPName + "&sub=" + oidcUpstreamSubjectQueryEscaped, wantDownstreamIDTokenUsername: "", // username scope was not requested wantDownstreamIDTokenGroups: oidcUpstreamGroupMembership, wantDownstreamRequestedScopes: []string{"openid", "groups", "offline_access"}, @@ -771,19 +847,19 @@ func TestCallbackEndpoint(t *testing.T) { wantDownstreamClientID: downstreamDynamicClientID, wantDownstreamPKCEChallenge: downstreamPKCEChallenge, wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod, - wantDownstreamCustomSessionData: happyDownstreamCustomSessionData, - wantAuthcodeExchangeCall: &expectedAuthcodeExchange{ - performedByUpstreamName: happyUpstreamIDPName, - args: happyExchangeAndValidateTokensArgs, + wantDownstreamCustomSessionData: happyDownstreamCustomSessionDataForOIDCUpstream, + wantOIDCAuthcodeExchangeCall: &expectedOIDCAuthcodeExchange{ + performedByUpstreamName: happyOIDCUpstreamIDPName, + args: happyOIDCUpstreamExchangeAuthcodeAndValidateTokenArgs, }, }, { name: "using dynamic client which is allowed to request groups scope, but does not actually request groups scope in authorize request, does not get groups in ID token", - idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC(happyUpstream().Build()), + idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC(happyOIDCUpstream().Build()), kubeResources: addFullyCapableDynamicClientAndSecretToKubeResources, method: http.MethodGet, path: newRequestPath().WithState( - happyUpstreamStateParamForDynamicClient(). + happyOIDCUpstreamStateParamForDynamicClient(). WithAuthorizeRequestParams(shallowCopyAndModifyQuery(happyDownstreamRequestParamsQueryForDynamicClient, map[string]string{"scope": "openid username offline_access"}).Encode()). Build(t, happyStateCodec), @@ -792,7 +868,7 @@ func TestCallbackEndpoint(t *testing.T) { wantStatus: http.StatusSeeOther, wantRedirectLocationRegexp: downstreamRedirectURI + `\?code=([^&]+)&scope=openid\+offline_access\+username&state=` + happyDownstreamState, wantBody: "", - wantDownstreamIDTokenSubject: oidcUpstreamIssuer + "?idpName=" + happyUpstreamIDPName + "&sub=" + oidcUpstreamSubjectQueryEscaped, + wantDownstreamIDTokenSubject: oidcUpstreamIssuer + "?idpName=" + happyOIDCUpstreamIDPName + "&sub=" + oidcUpstreamSubjectQueryEscaped, wantDownstreamIDTokenUsername: oidcUpstreamUsername, wantDownstreamIDTokenGroups: nil, // groups scope was not requested wantDownstreamRequestedScopes: []string{"openid", "username", "offline_access"}, @@ -801,15 +877,15 @@ func TestCallbackEndpoint(t *testing.T) { wantDownstreamClientID: downstreamDynamicClientID, wantDownstreamPKCEChallenge: downstreamPKCEChallenge, wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod, - wantDownstreamCustomSessionData: happyDownstreamCustomSessionData, - wantAuthcodeExchangeCall: &expectedAuthcodeExchange{ - performedByUpstreamName: happyUpstreamIDPName, - args: happyExchangeAndValidateTokensArgs, + wantDownstreamCustomSessionData: happyDownstreamCustomSessionDataForOIDCUpstream, + wantOIDCAuthcodeExchangeCall: &expectedOIDCAuthcodeExchange{ + performedByUpstreamName: happyOIDCUpstreamIDPName, + args: happyOIDCUpstreamExchangeAuthcodeAndValidateTokenArgs, }, }, { name: "using dynamic client which is not allowed to request username scope, and does not actually request username scope in authorize request, does not get username in ID token", - idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC(happyUpstream().Build()), + idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC(happyOIDCUpstream().Build()), kubeResources: func(t *testing.T, supervisorClient *supervisorfake.Clientset, kubeClient *fake.Clientset) { oidcClient, secret := testutil.OIDCClientAndStorageSecret(t, "some-namespace", downstreamDynamicClientID, downstreamDynamicClientUID, @@ -821,7 +897,7 @@ func TestCallbackEndpoint(t *testing.T) { }, method: http.MethodGet, path: newRequestPath().WithState( - happyUpstreamStateParam().WithAuthorizeRequestParams( + happyOIDCUpstreamStateParam().WithAuthorizeRequestParams( shallowCopyAndModifyQuery( happyDownstreamRequestParamsQuery, map[string]string{ @@ -835,7 +911,7 @@ func TestCallbackEndpoint(t *testing.T) { wantStatus: http.StatusSeeOther, wantRedirectLocationRegexp: downstreamRedirectURI + `\?code=([^&]+)&scope=openid\+offline_access\+groups&state=` + happyDownstreamState, wantBody: "", - wantDownstreamIDTokenSubject: oidcUpstreamIssuer + "?idpName=" + happyUpstreamIDPName + "&sub=" + oidcUpstreamSubjectQueryEscaped, + wantDownstreamIDTokenSubject: oidcUpstreamIssuer + "?idpName=" + happyOIDCUpstreamIDPName + "&sub=" + oidcUpstreamSubjectQueryEscaped, wantDownstreamIDTokenUsername: "", // username scope was not requested wantDownstreamIDTokenGroups: oidcUpstreamGroupMembership, wantDownstreamRequestedScopes: []string{"openid", "groups", "offline_access"}, @@ -844,15 +920,15 @@ func TestCallbackEndpoint(t *testing.T) { wantDownstreamClientID: downstreamDynamicClientID, wantDownstreamPKCEChallenge: downstreamPKCEChallenge, wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod, - wantDownstreamCustomSessionData: happyDownstreamCustomSessionData, - wantAuthcodeExchangeCall: &expectedAuthcodeExchange{ - performedByUpstreamName: happyUpstreamIDPName, - args: happyExchangeAndValidateTokensArgs, + wantDownstreamCustomSessionData: happyDownstreamCustomSessionDataForOIDCUpstream, + wantOIDCAuthcodeExchangeCall: &expectedOIDCAuthcodeExchange{ + performedByUpstreamName: happyOIDCUpstreamIDPName, + args: happyOIDCUpstreamExchangeAuthcodeAndValidateTokenArgs, }, }, { name: "using dynamic client which is not allowed to request groups scope, and does not actually request groups scope in authorize request, does not get groups in ID token", - idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC(happyUpstream().Build()), + idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC(happyOIDCUpstream().Build()), kubeResources: func(t *testing.T, supervisorClient *supervisorfake.Clientset, kubeClient *fake.Clientset) { oidcClient, secret := testutil.OIDCClientAndStorageSecret(t, "some-namespace", downstreamDynamicClientID, downstreamDynamicClientUID, @@ -864,7 +940,7 @@ func TestCallbackEndpoint(t *testing.T) { }, method: http.MethodGet, path: newRequestPath().WithState( - happyUpstreamStateParam().WithAuthorizeRequestParams( + happyOIDCUpstreamStateParam().WithAuthorizeRequestParams( shallowCopyAndModifyQuery( happyDownstreamRequestParamsQuery, map[string]string{ @@ -878,7 +954,7 @@ func TestCallbackEndpoint(t *testing.T) { wantStatus: http.StatusSeeOther, wantRedirectLocationRegexp: downstreamRedirectURI + `\?code=([^&]+)&scope=openid\+offline_access\+username&state=` + happyDownstreamState, wantBody: "", - wantDownstreamIDTokenSubject: oidcUpstreamIssuer + "?idpName=" + happyUpstreamIDPName + "&sub=" + oidcUpstreamSubjectQueryEscaped, + wantDownstreamIDTokenSubject: oidcUpstreamIssuer + "?idpName=" + happyOIDCUpstreamIDPName + "&sub=" + oidcUpstreamSubjectQueryEscaped, wantDownstreamIDTokenUsername: oidcUpstreamUsername, wantDownstreamIDTokenGroups: nil, // groups scope was not requested wantDownstreamRequestedScopes: []string{"openid", "username", "offline_access"}, @@ -887,23 +963,23 @@ func TestCallbackEndpoint(t *testing.T) { wantDownstreamClientID: downstreamDynamicClientID, wantDownstreamPKCEChallenge: downstreamPKCEChallenge, wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod, - wantDownstreamCustomSessionData: happyDownstreamCustomSessionData, - wantAuthcodeExchangeCall: &expectedAuthcodeExchange{ - performedByUpstreamName: happyUpstreamIDPName, - args: happyExchangeAndValidateTokensArgs, + wantDownstreamCustomSessionData: happyDownstreamCustomSessionDataForOIDCUpstream, + wantOIDCAuthcodeExchangeCall: &expectedOIDCAuthcodeExchange{ + performedByUpstreamName: happyOIDCUpstreamIDPName, + args: happyOIDCUpstreamExchangeAuthcodeAndValidateTokenArgs, }, }, { - name: "using identity transformations which modify the username and group names", + name: "OIDC: using identity transformations which modify the username and group names", idps: testidplister.NewUpstreamIDPListerBuilder(). - WithOIDC(happyUpstream().WithTransformsForFederationDomain(prefixUsernameAndGroupsPipeline).Build()), + WithOIDC(happyOIDCUpstream().WithTransformsForFederationDomain(prefixUsernameAndGroupsPipeline).Build()), method: http.MethodGet, - path: newRequestPath().WithState(happyState).String(), + path: newRequestPath().WithState(happyOIDCState).String(), csrfCookie: happyCSRFCookie, wantStatus: http.StatusSeeOther, wantRedirectLocationRegexp: happyDownstreamRedirectLocationRegexp, wantBody: "", - wantDownstreamIDTokenSubject: oidcUpstreamIssuer + "?idpName=" + happyUpstreamIDPName + "&sub=" + oidcUpstreamSubjectQueryEscaped, + wantDownstreamIDTokenSubject: oidcUpstreamIssuer + "?idpName=" + happyOIDCUpstreamIDPName + "&sub=" + oidcUpstreamSubjectQueryEscaped, wantDownstreamIDTokenUsername: transformationUsernamePrefix + oidcUpstreamUsername, wantDownstreamIDTokenGroups: testutil.AddPrefixToEach(transformationGroupsPrefix, oidcUpstreamGroupMembership), wantDownstreamRequestedScopes: happyDownstreamScopesRequested, @@ -913,20 +989,51 @@ func TestCallbackEndpoint(t *testing.T) { wantDownstreamPKCEChallenge: downstreamPKCEChallenge, wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod, wantDownstreamCustomSessionData: happyDownstreamCustomSessionDataWithUsernameAndGroups( + happyDownstreamCustomSessionDataForOIDCUpstream, transformationUsernamePrefix+oidcUpstreamUsername, oidcUpstreamUsername, oidcUpstreamGroupMembership, ), - wantAuthcodeExchangeCall: &expectedAuthcodeExchange{ - performedByUpstreamName: happyUpstreamIDPName, - args: happyExchangeAndValidateTokensArgs, + wantOIDCAuthcodeExchangeCall: &expectedOIDCAuthcodeExchange{ + performedByUpstreamName: happyOIDCUpstreamIDPName, + args: happyOIDCUpstreamExchangeAuthcodeAndValidateTokenArgs, + }, + }, + { + name: "GitHub: using identity transformations which modify the username and group names", + idps: testidplister.NewUpstreamIDPListerBuilder(). + WithGitHub(happyGitHubUpstream().WithTransformsForFederationDomain(prefixUsernameAndGroupsPipeline).Build()), + method: http.MethodGet, + path: happyGitHubPath, + csrfCookie: happyCSRFCookie, + wantStatus: http.StatusSeeOther, + wantRedirectLocationRegexp: happyDownstreamRedirectLocationRegexp, + wantBody: "", + wantDownstreamIDTokenSubject: githubDownstreamSubject, + wantDownstreamIDTokenUsername: transformationUsernamePrefix + githubUpstreamUsername, + wantDownstreamIDTokenGroups: testutil.AddPrefixToEach(transformationGroupsPrefix, githubUpstreamGroupMembership), + wantDownstreamRequestedScopes: happyDownstreamScopesRequested, + wantDownstreamGrantedScopes: happyDownstreamScopesGranted, + wantDownstreamNonce: downstreamNonce, + wantDownstreamClientID: downstreamPinnipedClientID, + wantDownstreamPKCEChallenge: downstreamPKCEChallenge, + wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod, + wantDownstreamCustomSessionData: happyDownstreamCustomSessionDataWithUsernameAndGroups( + happyDownstreamCustomSessionDataForGitHubUpstream, + transformationUsernamePrefix+githubUpstreamUsername, + githubUpstreamUsername, + githubUpstreamGroupMembership, + ), + wantGitHubAuthcodeExchangeCall: &expectedGitHubAuthcodeExchange{ + performedByUpstreamName: happyGithubIDPName, + args: happyGitHubUpstreamExchangeAuthcodeArgs, }, }, // Pre-upstream-exchange verification { name: "PUT method is invalid", - idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC(happyUpstream().Build()), + idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC(happyOIDCUpstream().Build()), method: http.MethodPut, path: newRequestPath().String(), wantStatus: http.StatusMethodNotAllowed, @@ -935,7 +1042,7 @@ func TestCallbackEndpoint(t *testing.T) { }, { name: "POST method is invalid", - idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC(happyUpstream().Build()), + idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC(happyOIDCUpstream().Build()), method: http.MethodPost, path: newRequestPath().String(), wantStatus: http.StatusMethodNotAllowed, @@ -944,7 +1051,7 @@ func TestCallbackEndpoint(t *testing.T) { }, { name: "PATCH method is invalid", - idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC(happyUpstream().Build()), + idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC(happyOIDCUpstream().Build()), method: http.MethodPatch, path: newRequestPath().String(), wantStatus: http.StatusMethodNotAllowed, @@ -953,7 +1060,7 @@ func TestCallbackEndpoint(t *testing.T) { }, { name: "DELETE method is invalid", - idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC(happyUpstream().Build()), + idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC(happyOIDCUpstream().Build()), method: http.MethodDelete, path: newRequestPath().String(), wantStatus: http.StatusMethodNotAllowed, @@ -962,9 +1069,9 @@ func TestCallbackEndpoint(t *testing.T) { }, { name: "code param was not included on request", - idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC(happyUpstream().Build()), + idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC(happyOIDCUpstream().Build()), method: http.MethodGet, - path: newRequestPath().WithState(happyState).WithoutCode().String(), + path: newRequestPath().WithState(happyOIDCState).WithoutCode().String(), csrfCookie: happyCSRFCookie, wantStatus: http.StatusBadRequest, wantContentType: htmlContentType, @@ -972,7 +1079,7 @@ func TestCallbackEndpoint(t *testing.T) { }, { name: "state param was not included on request", - idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC(happyUpstream().Build()), + idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC(happyOIDCUpstream().Build()), method: http.MethodGet, path: newRequestPath().WithoutState().String(), csrfCookie: happyCSRFCookie, @@ -982,7 +1089,7 @@ func TestCallbackEndpoint(t *testing.T) { }, { name: "state param was not signed correctly, has expired, or otherwise cannot be decoded for any reason", - idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC(happyUpstream().Build()), + idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC(happyOIDCUpstream().Build()), method: http.MethodGet, path: newRequestPath().WithState("this-will-not-decode").String(), csrfCookie: happyCSRFCookie, @@ -994,18 +1101,18 @@ func TestCallbackEndpoint(t *testing.T) { // This shouldn't happen in practice because the authorize endpoint should have already run the same // validations, but we would like to test the error handling in this endpoint anyway. name: "state param contains authorization request params which fail validation", - idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC(happyUpstream().Build()), + idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC(happyOIDCUpstream().Build()), method: http.MethodGet, path: newRequestPath().WithState( - happyUpstreamStateParam(). + happyOIDCUpstreamStateParam(). WithAuthorizeRequestParams(shallowCopyAndModifyQuery(happyDownstreamRequestParamsQuery, map[string]string{"prompt": "none login"}).Encode()). Build(t, happyStateCodec), ).String(), csrfCookie: happyCSRFCookie, - wantAuthcodeExchangeCall: &expectedAuthcodeExchange{ - performedByUpstreamName: happyUpstreamIDPName, - args: happyExchangeAndValidateTokensArgs, + wantOIDCAuthcodeExchangeCall: &expectedOIDCAuthcodeExchange{ + performedByUpstreamName: happyOIDCUpstreamIDPName, + args: happyOIDCUpstreamExchangeAuthcodeAndValidateTokenArgs, }, wantStatus: http.StatusInternalServerError, @@ -1014,9 +1121,9 @@ func TestCallbackEndpoint(t *testing.T) { }, { name: "state's internal version does not match what we want", - idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC(happyUpstream().Build()), + idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC(happyOIDCUpstream().Build()), method: http.MethodGet, - path: newRequestPath().WithState(happyUpstreamStateParam().WithStateVersion("wrong-state-version").Build(t, happyStateCodec)).String(), + path: newRequestPath().WithState(happyOIDCUpstreamStateParam().WithStateVersion("wrong-state-version").Build(t, happyStateCodec)).String(), csrfCookie: happyCSRFCookie, wantStatus: http.StatusUnprocessableEntity, wantContentType: htmlContentType, @@ -1024,9 +1131,9 @@ func TestCallbackEndpoint(t *testing.T) { }, { name: "state's downstream auth params element is invalid", - idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC(happyUpstream().Build()), + idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC(happyOIDCUpstream().Build()), method: http.MethodGet, - path: newRequestPath().WithState(happyUpstreamStateParam(). + path: newRequestPath().WithState(happyOIDCUpstreamStateParam(). WithAuthorizeRequestParams("the following is an invalid url encoding token, and therefore this is an invalid param: %z"). Build(t, happyStateCodec)).String(), csrfCookie: happyCSRFCookie, @@ -1036,10 +1143,10 @@ func TestCallbackEndpoint(t *testing.T) { }, { name: "state's downstream auth params are missing required value (e.g., client_id)", - idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC(happyUpstream().Build()), + idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC(happyOIDCUpstream().Build()), method: http.MethodGet, path: newRequestPath().WithState( - happyUpstreamStateParam(). + happyOIDCUpstreamStateParam(). WithAuthorizeRequestParams(shallowCopyAndModifyQuery(happyDownstreamRequestParamsQuery, map[string]string{"client_id": ""}).Encode()). Build(t, happyStateCodec), @@ -1051,10 +1158,10 @@ func TestCallbackEndpoint(t *testing.T) { }, { name: "state's downstream auth params have invalid client_id", - idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC(happyUpstream().Build()), + idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC(happyOIDCUpstream().Build()), method: http.MethodGet, path: newRequestPath().WithState( - happyUpstreamStateParam(). + happyOIDCUpstreamStateParam(). WithAuthorizeRequestParams(shallowCopyAndModifyQuery(happyDownstreamRequestParamsQuery, map[string]string{"client_id": "bogus"}).Encode()). Build(t, happyStateCodec), @@ -1066,11 +1173,11 @@ func TestCallbackEndpoint(t *testing.T) { }, { name: "dynamic clients do not allow response_mode=form_post", - idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC(happyUpstream().Build()), + idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC(happyOIDCUpstream().Build()), kubeResources: addFullyCapableDynamicClientAndSecretToKubeResources, method: http.MethodGet, path: newRequestPath().WithState( - happyUpstreamStateParam().WithAuthorizeRequestParams( + happyOIDCUpstreamStateParam().WithAuthorizeRequestParams( shallowCopyAndModifyQuery( happyDownstreamRequestParamsQuery, map[string]string{ @@ -1088,7 +1195,7 @@ func TestCallbackEndpoint(t *testing.T) { }, { name: "using dynamic client which is not allowed to request username scope in authorize request but requests it anyway", - idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC(happyUpstream().Build()), + idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC(happyOIDCUpstream().Build()), kubeResources: func(t *testing.T, supervisorClient *supervisorfake.Clientset, kubeClient *fake.Clientset) { oidcClient, secret := testutil.OIDCClientAndStorageSecret(t, "some-namespace", downstreamDynamicClientID, downstreamDynamicClientUID, @@ -1100,7 +1207,7 @@ func TestCallbackEndpoint(t *testing.T) { }, method: http.MethodGet, path: newRequestPath().WithState( - happyUpstreamStateParam().WithAuthorizeRequestParams( + happyOIDCUpstreamStateParam().WithAuthorizeRequestParams( shallowCopyAndModifyQuery( happyDownstreamRequestParamsQuery, map[string]string{ @@ -1117,7 +1224,7 @@ func TestCallbackEndpoint(t *testing.T) { }, { name: "using dynamic client which is not allowed to request groups scope in authorize request but requests it anyway", - idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC(happyUpstream().Build()), + idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC(happyOIDCUpstream().Build()), kubeResources: func(t *testing.T, supervisorClient *supervisorfake.Clientset, kubeClient *fake.Clientset) { oidcClient, secret := testutil.OIDCClientAndStorageSecret(t, "some-namespace", downstreamDynamicClientID, downstreamDynamicClientUID, @@ -1129,7 +1236,7 @@ func TestCallbackEndpoint(t *testing.T) { }, method: http.MethodGet, path: newRequestPath().WithState( - happyUpstreamStateParam().WithAuthorizeRequestParams( + happyOIDCUpstreamStateParam().WithAuthorizeRequestParams( shallowCopyAndModifyQuery( happyDownstreamRequestParamsQuery, map[string]string{ @@ -1146,11 +1253,11 @@ func TestCallbackEndpoint(t *testing.T) { }, { name: "state's downstream auth params does not contain openid scope", - idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC(happyUpstream().Build()), + idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC(happyOIDCUpstream().Build()), method: http.MethodGet, path: newRequestPath(). WithState( - happyUpstreamStateParam(). + happyOIDCUpstreamStateParam(). WithAuthorizeRequestParams(shallowCopyAndModifyQuery(happyDownstreamRequestParamsQuery, map[string]string{"scope": "profile username email groups"}).Encode()). Build(t, happyStateCodec), @@ -1159,7 +1266,7 @@ func TestCallbackEndpoint(t *testing.T) { wantStatus: http.StatusSeeOther, wantRedirectLocationRegexp: downstreamRedirectURI + `\?code=([^&]+)&scope=username\+groups&state=` + happyDownstreamState, wantDownstreamIDTokenUsername: oidcUpstreamUsername, - wantDownstreamIDTokenSubject: oidcUpstreamIssuer + "?idpName=" + happyUpstreamIDPName + "&sub=" + oidcUpstreamSubjectQueryEscaped, + wantDownstreamIDTokenSubject: oidcUpstreamIssuer + "?idpName=" + happyOIDCUpstreamIDPName + "&sub=" + oidcUpstreamSubjectQueryEscaped, wantDownstreamRequestedScopes: []string{"profile", "email", "username", "groups"}, wantDownstreamGrantedScopes: []string{"username", "groups"}, wantDownstreamIDTokenGroups: oidcUpstreamGroupMembership, @@ -1167,19 +1274,19 @@ func TestCallbackEndpoint(t *testing.T) { wantDownstreamClientID: downstreamPinnipedClientID, wantDownstreamPKCEChallenge: downstreamPKCEChallenge, wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod, - wantDownstreamCustomSessionData: happyDownstreamCustomSessionData, - wantAuthcodeExchangeCall: &expectedAuthcodeExchange{ - performedByUpstreamName: happyUpstreamIDPName, - args: happyExchangeAndValidateTokensArgs, + wantDownstreamCustomSessionData: happyDownstreamCustomSessionDataForOIDCUpstream, + wantOIDCAuthcodeExchangeCall: &expectedOIDCAuthcodeExchange{ + performedByUpstreamName: happyOIDCUpstreamIDPName, + args: happyOIDCUpstreamExchangeAuthcodeAndValidateTokenArgs, }, }, { name: "state's downstream auth params does not contain openid, username, or groups scope", - idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC(happyUpstream().Build()), + idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC(happyOIDCUpstream().Build()), method: http.MethodGet, path: newRequestPath(). WithState( - happyUpstreamStateParam(). + happyOIDCUpstreamStateParam(). WithAuthorizeRequestParams(shallowCopyAndModifyQuery(happyDownstreamRequestParamsQuery, map[string]string{"scope": "profile email"}).Encode()). Build(t, happyStateCodec), @@ -1189,7 +1296,7 @@ func TestCallbackEndpoint(t *testing.T) { wantRedirectLocationRegexp: downstreamRedirectURI + `\?code=([^&]+)&scope=username\+groups&state=` + happyDownstreamState, wantDownstreamIDTokenUsername: oidcUpstreamUsername, wantDownstreamIDTokenGroups: oidcUpstreamGroupMembership, - wantDownstreamIDTokenSubject: oidcUpstreamIssuer + "?idpName=" + happyUpstreamIDPName + "&sub=" + oidcUpstreamSubjectQueryEscaped, + wantDownstreamIDTokenSubject: oidcUpstreamIssuer + "?idpName=" + happyOIDCUpstreamIDPName + "&sub=" + oidcUpstreamSubjectQueryEscaped, wantDownstreamRequestedScopes: []string{"profile", "email"}, // username and groups scopes were not requested but are granted anyway for the pinniped-cli client for backwards compatibility wantDownstreamGrantedScopes: []string{"username", "groups"}, @@ -1197,19 +1304,19 @@ func TestCallbackEndpoint(t *testing.T) { wantDownstreamClientID: downstreamPinnipedClientID, wantDownstreamPKCEChallenge: downstreamPKCEChallenge, wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod, - wantDownstreamCustomSessionData: happyDownstreamCustomSessionData, - wantAuthcodeExchangeCall: &expectedAuthcodeExchange{ - performedByUpstreamName: happyUpstreamIDPName, - args: happyExchangeAndValidateTokensArgs, + wantDownstreamCustomSessionData: happyDownstreamCustomSessionDataForOIDCUpstream, + wantOIDCAuthcodeExchangeCall: &expectedOIDCAuthcodeExchange{ + performedByUpstreamName: happyOIDCUpstreamIDPName, + args: happyOIDCUpstreamExchangeAuthcodeAndValidateTokenArgs, }, }, { name: "state's downstream auth params also included offline_access scope", - idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC(happyUpstream().Build()), + idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC(happyOIDCUpstream().Build()), method: http.MethodGet, path: newRequestPath(). WithState( - happyUpstreamStateParam(). + happyOIDCUpstreamStateParam(). WithAuthorizeRequestParams(shallowCopyAndModifyQuery(happyDownstreamRequestParamsQuery, map[string]string{"scope": "openid offline_access username groups"}).Encode()). Build(t, happyStateCodec), @@ -1218,7 +1325,7 @@ func TestCallbackEndpoint(t *testing.T) { wantStatus: http.StatusSeeOther, wantRedirectLocationRegexp: downstreamRedirectURI + `\?code=([^&]+)&scope=openid\+offline_access\+username\+groups&state=` + happyDownstreamState, wantDownstreamIDTokenUsername: oidcUpstreamUsername, - wantDownstreamIDTokenSubject: oidcUpstreamIssuer + "?idpName=" + happyUpstreamIDPName + "&sub=" + oidcUpstreamSubjectQueryEscaped, + wantDownstreamIDTokenSubject: oidcUpstreamIssuer + "?idpName=" + happyOIDCUpstreamIDPName + "&sub=" + oidcUpstreamSubjectQueryEscaped, wantDownstreamRequestedScopes: []string{"openid", "offline_access", "username", "groups"}, wantDownstreamGrantedScopes: []string{"openid", "offline_access", "username", "groups"}, wantDownstreamIDTokenGroups: oidcUpstreamGroupMembership, @@ -1226,17 +1333,81 @@ func TestCallbackEndpoint(t *testing.T) { wantDownstreamClientID: downstreamPinnipedClientID, wantDownstreamPKCEChallenge: downstreamPKCEChallenge, wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod, - wantDownstreamCustomSessionData: happyDownstreamCustomSessionData, - wantAuthcodeExchangeCall: &expectedAuthcodeExchange{ - performedByUpstreamName: happyUpstreamIDPName, - args: happyExchangeAndValidateTokensArgs, + wantDownstreamCustomSessionData: happyDownstreamCustomSessionDataForOIDCUpstream, + wantOIDCAuthcodeExchangeCall: &expectedOIDCAuthcodeExchange{ + performedByUpstreamName: happyOIDCUpstreamIDPName, + args: happyOIDCUpstreamExchangeAuthcodeAndValidateTokenArgs, + }, + }, + { + name: "GitHub: GET with good state and cookie and successful upstream token exchange returns 303 to downstream client callback", + idps: testidplister.NewUpstreamIDPListerBuilder().WithGitHub(happyGitHubUpstream().Build()), + method: http.MethodGet, + path: newRequestPath().WithState( + happyGitHubUpstreamStateParam(). + WithAuthorizeRequestParams( + happyDownstreamRequestParamsQuery.Encode(), + ).Build(t, happyStateCodec), + ).String(), + csrfCookie: happyCSRFCookie, + wantStatus: http.StatusSeeOther, + wantRedirectLocationRegexp: happyDownstreamRedirectLocationRegexp, + wantBody: "", + wantDownstreamIDTokenSubject: githubDownstreamSubject, + wantDownstreamIDTokenUsername: githubUpstreamUsername, + wantDownstreamIDTokenGroups: githubUpstreamGroupMembership, + wantDownstreamRequestedScopes: happyDownstreamScopesRequested, + wantDownstreamGrantedScopes: happyDownstreamScopesGranted, + wantDownstreamNonce: downstreamNonce, + wantDownstreamClientID: downstreamPinnipedClientID, + wantDownstreamPKCEChallenge: downstreamPKCEChallenge, + wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod, + wantDownstreamCustomSessionData: happyDownstreamCustomSessionDataForGitHubUpstream, + wantGitHubAuthcodeExchangeCall: &expectedGitHubAuthcodeExchange{ + performedByUpstreamName: happyGithubIDPName, + args: happyGitHubUpstreamExchangeAuthcodeArgs, + }, + }, + { + name: "GitHub: GET with good state and cookie and successful upstream token exchange with dynamic client returns 303 to downstream client callback, with dynamic client", + idps: testidplister.NewUpstreamIDPListerBuilder().WithGitHub(happyGitHubUpstream().Build()), + method: http.MethodGet, + kubeResources: addFullyCapableDynamicClientAndSecretToKubeResources, + path: newRequestPath().WithState( + happyGitHubUpstreamStateParam(). + WithAuthorizeRequestParams( + shallowCopyAndModifyQuery( + happyDownstreamRequestParamsQuery, + map[string]string{ + "client_id": downstreamDynamicClientID, + }, + ).Encode(), + ).Build(t, happyStateCodec), + ).String(), + csrfCookie: happyCSRFCookie, + wantStatus: http.StatusSeeOther, + wantRedirectLocationRegexp: happyDownstreamRedirectLocationRegexp, + wantBody: "", + wantDownstreamIDTokenSubject: githubDownstreamSubject, + wantDownstreamIDTokenUsername: githubUpstreamUsername, + wantDownstreamIDTokenGroups: githubUpstreamGroupMembership, + wantDownstreamRequestedScopes: happyDownstreamScopesRequested, + wantDownstreamGrantedScopes: happyDownstreamScopesGranted, + wantDownstreamNonce: downstreamNonce, + wantDownstreamClientID: downstreamDynamicClientID, + wantDownstreamPKCEChallenge: downstreamPKCEChallenge, + wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod, + wantDownstreamCustomSessionData: happyDownstreamCustomSessionDataForGitHubUpstream, + wantGitHubAuthcodeExchangeCall: &expectedGitHubAuthcodeExchange{ + performedByUpstreamName: happyGithubIDPName, + args: happyGitHubUpstreamExchangeAuthcodeArgs, }, }, { name: "the OIDCIdentityProvider resource has been deleted", idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC(otherUpstreamOIDCIdentityProvider), method: http.MethodGet, - path: newRequestPath().WithState(happyState).String(), + path: newRequestPath().WithState(happyOIDCState).String(), csrfCookie: happyCSRFCookie, wantStatus: http.StatusUnprocessableEntity, wantContentType: htmlContentType, @@ -1244,18 +1415,18 @@ func TestCallbackEndpoint(t *testing.T) { }, { name: "the CSRF cookie does not exist on request", - idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC(happyUpstream().Build()), + idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC(happyOIDCUpstream().Build()), method: http.MethodGet, - path: newRequestPath().WithState(happyState).String(), + path: newRequestPath().WithState(happyOIDCState).String(), wantStatus: http.StatusForbidden, wantContentType: htmlContentType, wantBody: "Forbidden: CSRF cookie is missing\n", }, { name: "cookie was not signed correctly, has expired, or otherwise cannot be decoded for any reason", - idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC(happyUpstream().Build()), + idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC(happyOIDCUpstream().Build()), method: http.MethodGet, - path: newRequestPath().WithState(happyState).String(), + path: newRequestPath().WithState(happyOIDCState).String(), csrfCookie: "__Host-pinniped-csrf=this-value-was-not-signed-by-pinniped", wantStatus: http.StatusForbidden, wantContentType: htmlContentType, @@ -1263,9 +1434,9 @@ func TestCallbackEndpoint(t *testing.T) { }, { name: "cookie csrf value does not match state csrf value", - idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC(happyUpstream().Build()), + idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC(happyOIDCUpstream().Build()), method: http.MethodGet, - path: newRequestPath().WithState(happyUpstreamStateParam().WithCSRF("wrong-csrf-value").Build(t, happyStateCodec)).String(), + path: newRequestPath().WithState(happyOIDCUpstreamStateParam().WithCSRF("wrong-csrf-value").Build(t, happyStateCodec)).String(), csrfCookie: happyCSRFCookie, wantStatus: http.StatusForbidden, wantContentType: htmlContentType, @@ -1274,49 +1445,70 @@ func TestCallbackEndpoint(t *testing.T) { // Upstream exchange { - name: "upstream auth code exchange fails", + name: "OIDC: upstream auth code exchange fails", idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC( - happyUpstream().WithUpstreamAuthcodeExchangeError(errors.New("some error")).Build(), + happyOIDCUpstream().WithUpstreamAuthcodeExchangeError(errors.New("some error")).Build(), ), method: http.MethodGet, - path: newRequestPath().WithState(happyState).String(), + path: newRequestPath().WithState(happyOIDCState).String(), csrfCookie: happyCSRFCookie, wantStatus: http.StatusBadGateway, wantBody: "Bad Gateway: error exchanging and validating upstream tokens\n", wantContentType: htmlContentType, - wantAuthcodeExchangeCall: &expectedAuthcodeExchange{ - performedByUpstreamName: happyUpstreamIDPName, - args: happyExchangeAndValidateTokensArgs, + wantOIDCAuthcodeExchangeCall: &expectedOIDCAuthcodeExchange{ + performedByUpstreamName: happyOIDCUpstreamIDPName, + args: happyOIDCUpstreamExchangeAuthcodeAndValidateTokenArgs, + }, + }, + { + name: "GitHub: upstream auth code exchange fails", + idps: testidplister.NewUpstreamIDPListerBuilder().WithGitHub( + happyGitHubUpstream().WithAuthcodeExchangeError(errors.New("some error")).Build(), + ), + method: http.MethodGet, + path: newRequestPath().WithState( + happyGitHubUpstreamStateParam(). + WithAuthorizeRequestParams( + happyDownstreamRequestParamsQuery.Encode(), + ).Build(t, happyStateCodec), + ).String(), + csrfCookie: happyCSRFCookie, + wantStatus: http.StatusBadGateway, + wantBody: "Bad Gateway: failed to exchange authcode using GitHub API\n", + wantContentType: htmlContentType, + wantGitHubAuthcodeExchangeCall: &expectedGitHubAuthcodeExchange{ + performedByUpstreamName: happyGithubIDPName, + args: happyGitHubUpstreamExchangeAuthcodeArgs, }, }, { name: "upstream ID token does not contain requested username claim", idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC( - happyUpstream().WithoutIDTokenClaim(oidcUpstreamUsernameClaim).Build(), + happyOIDCUpstream().WithoutIDTokenClaim(oidcUpstreamUsernameClaim).Build(), ), method: http.MethodGet, - path: newRequestPath().WithState(happyState).String(), + path: newRequestPath().WithState(happyOIDCState).String(), csrfCookie: happyCSRFCookie, wantStatus: http.StatusUnprocessableEntity, wantBody: "Unprocessable Entity: required claim in upstream ID token missing\n", wantContentType: htmlContentType, - wantAuthcodeExchangeCall: &expectedAuthcodeExchange{ - performedByUpstreamName: happyUpstreamIDPName, - args: happyExchangeAndValidateTokensArgs, + wantOIDCAuthcodeExchangeCall: &expectedOIDCAuthcodeExchange{ + performedByUpstreamName: happyOIDCUpstreamIDPName, + args: happyOIDCUpstreamExchangeAuthcodeAndValidateTokenArgs, }, }, { name: "upstream ID token does not contain requested groups claim", idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC( - happyUpstream().WithoutIDTokenClaim(oidcUpstreamGroupsClaim).Build(), + happyOIDCUpstream().WithoutIDTokenClaim(oidcUpstreamGroupsClaim).Build(), ), method: http.MethodGet, - path: newRequestPath().WithState(happyState).String(), + path: newRequestPath().WithState(happyOIDCState).String(), csrfCookie: happyCSRFCookie, wantStatus: http.StatusSeeOther, wantRedirectLocationRegexp: happyDownstreamRedirectLocationRegexp, wantBody: "", - wantDownstreamIDTokenSubject: oidcUpstreamIssuer + "?idpName=" + happyUpstreamIDPName + "&sub=" + oidcUpstreamSubjectQueryEscaped, + wantDownstreamIDTokenSubject: oidcUpstreamIssuer + "?idpName=" + happyOIDCUpstreamIDPName + "&sub=" + oidcUpstreamSubjectQueryEscaped, wantDownstreamIDTokenUsername: oidcUpstreamUsername, wantDownstreamRequestedScopes: happyDownstreamScopesRequested, wantDownstreamGrantedScopes: happyDownstreamScopesGranted, @@ -1326,204 +1518,220 @@ func TestCallbackEndpoint(t *testing.T) { wantDownstreamPKCEChallenge: downstreamPKCEChallenge, wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod, wantDownstreamCustomSessionData: happyDownstreamCustomSessionDataWithUsernameAndGroups( + happyDownstreamCustomSessionDataForOIDCUpstream, oidcUpstreamUsername, oidcUpstreamUsername, nil, ), - wantAuthcodeExchangeCall: &expectedAuthcodeExchange{ - performedByUpstreamName: happyUpstreamIDPName, - args: happyExchangeAndValidateTokensArgs, + wantOIDCAuthcodeExchangeCall: &expectedOIDCAuthcodeExchange{ + performedByUpstreamName: happyOIDCUpstreamIDPName, + args: happyOIDCUpstreamExchangeAuthcodeAndValidateTokenArgs, }, }, { name: "upstream ID token contains username claim with weird format", idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC( - happyUpstream().WithIDTokenClaim(oidcUpstreamUsernameClaim, 42).Build(), + happyOIDCUpstream().WithIDTokenClaim(oidcUpstreamUsernameClaim, 42).Build(), ), method: http.MethodGet, - path: newRequestPath().WithState(happyState).String(), + path: newRequestPath().WithState(happyOIDCState).String(), csrfCookie: happyCSRFCookie, wantStatus: http.StatusUnprocessableEntity, wantContentType: htmlContentType, wantBody: "Unprocessable Entity: required claim in upstream ID token has invalid format\n", - wantAuthcodeExchangeCall: &expectedAuthcodeExchange{ - performedByUpstreamName: happyUpstreamIDPName, - args: happyExchangeAndValidateTokensArgs, + wantOIDCAuthcodeExchangeCall: &expectedOIDCAuthcodeExchange{ + performedByUpstreamName: happyOIDCUpstreamIDPName, + args: happyOIDCUpstreamExchangeAuthcodeAndValidateTokenArgs, }, }, { name: "upstream ID token contains username claim with empty string value", idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC( - happyUpstream().WithIDTokenClaim(oidcUpstreamUsernameClaim, "").Build(), + happyOIDCUpstream().WithIDTokenClaim(oidcUpstreamUsernameClaim, "").Build(), ), method: http.MethodGet, - path: newRequestPath().WithState(happyState).String(), + path: newRequestPath().WithState(happyOIDCState).String(), csrfCookie: happyCSRFCookie, wantStatus: http.StatusUnprocessableEntity, wantContentType: htmlContentType, wantBody: "Unprocessable Entity: required claim in upstream ID token is empty\n", - wantAuthcodeExchangeCall: &expectedAuthcodeExchange{ - performedByUpstreamName: happyUpstreamIDPName, - args: happyExchangeAndValidateTokensArgs, + wantOIDCAuthcodeExchangeCall: &expectedOIDCAuthcodeExchange{ + performedByUpstreamName: happyOIDCUpstreamIDPName, + args: happyOIDCUpstreamExchangeAuthcodeAndValidateTokenArgs, }, }, { name: "upstream ID token does not contain iss claim when using default username claim config", idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC( - happyUpstream().WithoutIDTokenClaim("iss").WithoutUsernameClaim().Build(), + happyOIDCUpstream().WithoutIDTokenClaim("iss").WithoutUsernameClaim().Build(), ), method: http.MethodGet, - path: newRequestPath().WithState(happyState).String(), + path: newRequestPath().WithState(happyOIDCState).String(), csrfCookie: happyCSRFCookie, wantStatus: http.StatusUnprocessableEntity, wantContentType: htmlContentType, wantBody: "Unprocessable Entity: required claim in upstream ID token missing\n", - wantAuthcodeExchangeCall: &expectedAuthcodeExchange{ - performedByUpstreamName: happyUpstreamIDPName, - args: happyExchangeAndValidateTokensArgs, + wantOIDCAuthcodeExchangeCall: &expectedOIDCAuthcodeExchange{ + performedByUpstreamName: happyOIDCUpstreamIDPName, + args: happyOIDCUpstreamExchangeAuthcodeAndValidateTokenArgs, }, }, { name: "upstream ID token does has an empty string value for iss claim when using default username claim config", idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC( - happyUpstream().WithIDTokenClaim("iss", "").WithoutUsernameClaim().Build(), + happyOIDCUpstream().WithIDTokenClaim("iss", "").WithoutUsernameClaim().Build(), ), method: http.MethodGet, - path: newRequestPath().WithState(happyState).String(), + path: newRequestPath().WithState(happyOIDCState).String(), csrfCookie: happyCSRFCookie, wantStatus: http.StatusUnprocessableEntity, wantContentType: htmlContentType, wantBody: "Unprocessable Entity: required claim in upstream ID token is empty\n", - wantAuthcodeExchangeCall: &expectedAuthcodeExchange{ - performedByUpstreamName: happyUpstreamIDPName, - args: happyExchangeAndValidateTokensArgs, + wantOIDCAuthcodeExchangeCall: &expectedOIDCAuthcodeExchange{ + performedByUpstreamName: happyOIDCUpstreamIDPName, + args: happyOIDCUpstreamExchangeAuthcodeAndValidateTokenArgs, }, }, { name: "upstream ID token has an non-string iss claim when using default username claim config", idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC( - happyUpstream().WithIDTokenClaim("iss", 42).WithoutUsernameClaim().Build(), + happyOIDCUpstream().WithIDTokenClaim("iss", 42).WithoutUsernameClaim().Build(), ), method: http.MethodGet, - path: newRequestPath().WithState(happyState).String(), + path: newRequestPath().WithState(happyOIDCState).String(), csrfCookie: happyCSRFCookie, wantStatus: http.StatusUnprocessableEntity, wantContentType: htmlContentType, wantBody: "Unprocessable Entity: required claim in upstream ID token has invalid format\n", - wantAuthcodeExchangeCall: &expectedAuthcodeExchange{ - performedByUpstreamName: happyUpstreamIDPName, - args: happyExchangeAndValidateTokensArgs, + wantOIDCAuthcodeExchangeCall: &expectedOIDCAuthcodeExchange{ + performedByUpstreamName: happyOIDCUpstreamIDPName, + args: happyOIDCUpstreamExchangeAuthcodeAndValidateTokenArgs, }, }, { name: "upstream ID token does not contain sub claim when using default username claim config", idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC( - happyUpstream().WithoutIDTokenClaim("sub").WithoutUsernameClaim().Build(), + happyOIDCUpstream().WithoutIDTokenClaim("sub").WithoutUsernameClaim().Build(), ), method: http.MethodGet, - path: newRequestPath().WithState(happyState).String(), + path: newRequestPath().WithState(happyOIDCState).String(), csrfCookie: happyCSRFCookie, wantStatus: http.StatusUnprocessableEntity, wantContentType: htmlContentType, wantBody: "Unprocessable Entity: required claim in upstream ID token missing\n", - wantAuthcodeExchangeCall: &expectedAuthcodeExchange{ - performedByUpstreamName: happyUpstreamIDPName, - args: happyExchangeAndValidateTokensArgs, + wantOIDCAuthcodeExchangeCall: &expectedOIDCAuthcodeExchange{ + performedByUpstreamName: happyOIDCUpstreamIDPName, + args: happyOIDCUpstreamExchangeAuthcodeAndValidateTokenArgs, }, }, { name: "upstream ID token does has an empty string value for sub claim when using default username claim config", idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC( - happyUpstream().WithIDTokenClaim("sub", "").WithoutUsernameClaim().Build(), + happyOIDCUpstream().WithIDTokenClaim("sub", "").WithoutUsernameClaim().Build(), ), method: http.MethodGet, - path: newRequestPath().WithState(happyState).String(), + path: newRequestPath().WithState(happyOIDCState).String(), csrfCookie: happyCSRFCookie, wantStatus: http.StatusUnprocessableEntity, wantContentType: htmlContentType, wantBody: "Unprocessable Entity: required claim in upstream ID token is empty\n", - wantAuthcodeExchangeCall: &expectedAuthcodeExchange{ - performedByUpstreamName: happyUpstreamIDPName, - args: happyExchangeAndValidateTokensArgs, + wantOIDCAuthcodeExchangeCall: &expectedOIDCAuthcodeExchange{ + performedByUpstreamName: happyOIDCUpstreamIDPName, + args: happyOIDCUpstreamExchangeAuthcodeAndValidateTokenArgs, }, }, { name: "upstream ID token has an non-string sub claim when using default username claim config", idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC( - happyUpstream().WithIDTokenClaim("sub", 42).WithoutUsernameClaim().Build(), + happyOIDCUpstream().WithIDTokenClaim("sub", 42).WithoutUsernameClaim().Build(), ), method: http.MethodGet, - path: newRequestPath().WithState(happyState).String(), + path: newRequestPath().WithState(happyOIDCState).String(), csrfCookie: happyCSRFCookie, wantStatus: http.StatusUnprocessableEntity, wantContentType: htmlContentType, wantBody: "Unprocessable Entity: required claim in upstream ID token has invalid format\n", - wantAuthcodeExchangeCall: &expectedAuthcodeExchange{ - performedByUpstreamName: happyUpstreamIDPName, - args: happyExchangeAndValidateTokensArgs, + wantOIDCAuthcodeExchangeCall: &expectedOIDCAuthcodeExchange{ + performedByUpstreamName: happyOIDCUpstreamIDPName, + args: happyOIDCUpstreamExchangeAuthcodeAndValidateTokenArgs, }, }, { name: "upstream ID token contains groups claim with weird format", idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC( - happyUpstream().WithIDTokenClaim(oidcUpstreamGroupsClaim, 42).Build(), + happyOIDCUpstream().WithIDTokenClaim(oidcUpstreamGroupsClaim, 42).Build(), ), method: http.MethodGet, - path: newRequestPath().WithState(happyState).String(), + path: newRequestPath().WithState(happyOIDCState).String(), csrfCookie: happyCSRFCookie, wantStatus: http.StatusUnprocessableEntity, wantContentType: htmlContentType, wantBody: "Unprocessable Entity: required claim in upstream ID token has invalid format\n", - wantAuthcodeExchangeCall: &expectedAuthcodeExchange{ - performedByUpstreamName: happyUpstreamIDPName, - args: happyExchangeAndValidateTokensArgs, + wantOIDCAuthcodeExchangeCall: &expectedOIDCAuthcodeExchange{ + performedByUpstreamName: happyOIDCUpstreamIDPName, + args: happyOIDCUpstreamExchangeAuthcodeAndValidateTokenArgs, }, }, { name: "upstream ID token contains groups claim where one element is invalid", idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC( - happyUpstream().WithIDTokenClaim(oidcUpstreamGroupsClaim, []any{"foo", 7}).Build(), + happyOIDCUpstream().WithIDTokenClaim(oidcUpstreamGroupsClaim, []interface{}{"foo", 7}).Build(), ), method: http.MethodGet, - path: newRequestPath().WithState(happyState).String(), + path: newRequestPath().WithState(happyOIDCState).String(), csrfCookie: happyCSRFCookie, wantStatus: http.StatusUnprocessableEntity, wantContentType: htmlContentType, wantBody: "Unprocessable Entity: required claim in upstream ID token has invalid format\n", - wantAuthcodeExchangeCall: &expectedAuthcodeExchange{ - performedByUpstreamName: happyUpstreamIDPName, - args: happyExchangeAndValidateTokensArgs, + wantOIDCAuthcodeExchangeCall: &expectedOIDCAuthcodeExchange{ + performedByUpstreamName: happyOIDCUpstreamIDPName, + args: happyOIDCUpstreamExchangeAuthcodeAndValidateTokenArgs, }, }, { name: "upstream ID token contains groups claim with invalid null type", idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC( - happyUpstream().WithIDTokenClaim(oidcUpstreamGroupsClaim, nil).Build(), + happyOIDCUpstream().WithIDTokenClaim(oidcUpstreamGroupsClaim, nil).Build(), ), method: http.MethodGet, - path: newRequestPath().WithState(happyState).String(), + path: newRequestPath().WithState(happyOIDCState).String(), csrfCookie: happyCSRFCookie, wantStatus: http.StatusUnprocessableEntity, wantContentType: htmlContentType, wantBody: "Unprocessable Entity: required claim in upstream ID token has invalid format\n", - wantAuthcodeExchangeCall: &expectedAuthcodeExchange{ - performedByUpstreamName: happyUpstreamIDPName, - args: happyExchangeAndValidateTokensArgs, + wantOIDCAuthcodeExchangeCall: &expectedOIDCAuthcodeExchange{ + performedByUpstreamName: happyOIDCUpstreamIDPName, + args: happyOIDCUpstreamExchangeAuthcodeAndValidateTokenArgs, }, }, { - name: "using identity transformations which reject the authentication", + name: "OIDC: using identity transformations which reject the authentication", idps: testidplister.NewUpstreamIDPListerBuilder(). - WithOIDC(happyUpstream().WithTransformsForFederationDomain(rejectAuthPipeline).Build()), + WithOIDC(happyOIDCUpstream().WithTransformsForFederationDomain(rejectAuthPipeline).Build()), method: http.MethodGet, - path: newRequestPath().WithState(happyState).String(), + path: newRequestPath().WithState(happyOIDCState).String(), csrfCookie: happyCSRFCookie, wantStatus: http.StatusUnprocessableEntity, wantContentType: htmlContentType, wantBody: "Unprocessable Entity: configured identity policy rejected this authentication: authentication was rejected by a configured policy\n", - wantAuthcodeExchangeCall: &expectedAuthcodeExchange{ - performedByUpstreamName: happyUpstreamIDPName, - args: happyExchangeAndValidateTokensArgs, + wantOIDCAuthcodeExchangeCall: &expectedOIDCAuthcodeExchange{ + performedByUpstreamName: happyOIDCUpstreamIDPName, + args: happyOIDCUpstreamExchangeAuthcodeAndValidateTokenArgs, + }, + }, + { + name: "GitHub: using identity transformations which reject the authentication", + idps: testidplister.NewUpstreamIDPListerBuilder(). + WithGitHub(happyGitHubUpstream().WithTransformsForFederationDomain(rejectAuthPipeline).Build()), + method: http.MethodGet, + path: happyGitHubPath, + csrfCookie: happyCSRFCookie, + wantStatus: http.StatusUnprocessableEntity, + wantContentType: htmlContentType, + wantBody: "Unprocessable Entity: configured identity policy rejected this authentication: authentication was rejected by a configured policy\n", + wantGitHubAuthcodeExchangeCall: &expectedGitHubAuthcodeExchange{ + performedByUpstreamName: happyGithubIDPName, + args: happyGitHubUpstreamExchangeAuthcodeArgs, }, }, } @@ -1540,7 +1748,7 @@ func TestCallbackEndpoint(t *testing.T) { } // Configure fosite the same way that the production code would. - // Inject this into our test subject at the last second so we get a fresh storage for every test. + // Inject this into our test subject at the last second, so we get a fresh storage for every test. timeoutsConfiguration := oidc.DefaultOIDCTimeoutsConfiguration() // Use lower minimum required bcrypt cost than we would use in production to keep unit the tests fast. oauthStore := storage.NewKubeStorage(secrets, oidcClientsClient, timeoutsConfiguration, bcrypt.MinCost) @@ -1562,13 +1770,21 @@ func TestCallbackEndpoint(t *testing.T) { testutil.RequireSecurityHeadersWithFormPostPageCSPs(t, rsp) - if test.wantAuthcodeExchangeCall != nil { - test.wantAuthcodeExchangeCall.args.Ctx = reqContext - test.idps.RequireExactlyOneCallToExchangeAuthcodeAndValidateTokens(t, - test.wantAuthcodeExchangeCall.performedByUpstreamName, test.wantAuthcodeExchangeCall.args, + switch { + case test.wantOIDCAuthcodeExchangeCall != nil: + test.wantOIDCAuthcodeExchangeCall.args.Ctx = reqContext + test.idps.RequireExactlyOneOIDCAuthcodeExchange(t, + test.wantOIDCAuthcodeExchangeCall.performedByUpstreamName, + test.wantOIDCAuthcodeExchangeCall.args, ) - } else { - test.idps.RequireExactlyZeroCallsToExchangeAuthcodeAndValidateTokens(t) + case test.wantGitHubAuthcodeExchangeCall != nil: + test.wantGitHubAuthcodeExchangeCall.args.Ctx = reqContext + test.idps.RequireExactlyOneGitHubAuthcodeExchange(t, + test.wantGitHubAuthcodeExchangeCall.performedByUpstreamName, + test.wantGitHubAuthcodeExchangeCall.args, + ) + default: + test.idps.RequireExactlyZeroAuthcodeExchanges(t) } require.Equal(t, test.wantStatus, rsp.Code) @@ -1634,11 +1850,16 @@ func TestCallbackEndpoint(t *testing.T) { } } -type expectedAuthcodeExchange struct { +type expectedOIDCAuthcodeExchange struct { performedByUpstreamName string args *oidctestutil.ExchangeAuthcodeAndValidateTokenArgs } +type expectedGitHubAuthcodeExchange struct { + performedByUpstreamName string + args *oidctestutil.ExchangeAuthcodeArgs +} + type requestPath struct { code, state *string } @@ -1684,28 +1905,40 @@ func (r *requestPath) String() string { return path + params.Encode() } -func happyUpstreamStateParam() *oidctestutil.UpstreamStateParamBuilder { +func happyOIDCUpstreamStateParam() *oidctestutil.UpstreamStateParamBuilder { return &oidctestutil.UpstreamStateParamBuilder{ - U: happyUpstreamIDPName, + U: happyOIDCUpstreamIDPName, P: happyDownstreamRequestParams, T: "oidc", N: happyDownstreamNonce, C: happyDownstreamCSRF, - K: happyDownstreamPKCE, + K: happyDownstreamPKCEVerifier, V: happyDownstreamStateVersion, } } -func happyUpstreamStateParamForDynamicClient() *oidctestutil.UpstreamStateParamBuilder { - p := happyUpstreamStateParam() +func happyGitHubUpstreamStateParam() *oidctestutil.UpstreamStateParamBuilder { + return &oidctestutil.UpstreamStateParamBuilder{ + U: happyGithubIDPName, + P: happyDownstreamRequestParams, + T: "github", + N: happyDownstreamNonce, + C: happyDownstreamCSRF, + K: happyDownstreamPKCEVerifier, + V: happyDownstreamStateVersion, + } +} + +func happyOIDCUpstreamStateParamForDynamicClient() *oidctestutil.UpstreamStateParamBuilder { + p := happyOIDCUpstreamStateParam() p.P = happyDownstreamRequestParamsForDynamicClient return p } -func happyUpstream() *oidctestutil.TestUpstreamOIDCIdentityProviderBuilder { +func happyOIDCUpstream() *oidctestutil.TestUpstreamOIDCIdentityProviderBuilder { return oidctestutil.NewTestUpstreamOIDCIdentityProviderBuilder(). - WithName(happyUpstreamIDPName). - WithResourceUID(happyUpstreamIDPResourceUID). + WithName(happyOIDCUpstreamIDPName). + WithResourceUID(happyOIDCUpstreamIDPResourceUID). WithClientID("some-client-id"). WithScopes([]string{"scope1", "scope2"}). WithUsernameClaim(oidcUpstreamUsernameClaim). @@ -1720,6 +1953,19 @@ func happyUpstream() *oidctestutil.TestUpstreamOIDCIdentityProviderBuilder { WithPasswordGrantError(errors.New("the callback endpoint should not use password grants")) } +func happyGitHubUpstream() *oidctestutil.TestUpstreamGitHubIdentityProviderBuilder { + return oidctestutil.NewTestUpstreamGitHubIdentityProviderBuilder(). + WithName(happyGithubIDPName). + WithResourceUID(happyGithubIDPResourceUID). + WithClientID("some-client-id"). + WithAccessToken(githubUpstreamAccessToken). + WithUser(&upstreamprovider.GitHubUser{ + Username: githubUpstreamUsername, + Groups: githubUpstreamGroupMembership, + DownstreamSubject: githubDownstreamSubject, + }) +} + func shallowCopyAndModifyQuery(query url.Values, modifications map[string]string) url.Values { copied := url.Values{} for key, value := range query { diff --git a/internal/federationdomain/endpoints/idpdiscovery/idp_discovery_handler.go b/internal/federationdomain/endpoints/idpdiscovery/idp_discovery_handler.go index aef99618c..7f96b0128 100644 --- a/internal/federationdomain/endpoints/idpdiscovery/idp_discovery_handler.go +++ b/internal/federationdomain/endpoints/idpdiscovery/idp_discovery_handler.go @@ -40,6 +40,7 @@ func responseAsJSON(upstreamIDPs federationdomainproviders.FederationDomainIdent r := v1alpha1.IDPDiscoveryResponse{ PinnipedSupportedIDPTypes: []v1alpha1.PinnipedSupportedIDPType{ {Type: v1alpha1.IDPTypeActiveDirectory}, + {Type: v1alpha1.IDPTypeGitHub}, {Type: v1alpha1.IDPTypeLDAP}, {Type: v1alpha1.IDPTypeOIDC}, }, diff --git a/internal/federationdomain/endpoints/idpdiscovery/idp_discovery_handler_test.go b/internal/federationdomain/endpoints/idpdiscovery/idp_discovery_handler_test.go index 95579197e..cd386d457 100644 --- a/internal/federationdomain/endpoints/idpdiscovery/idp_discovery_handler_test.go +++ b/internal/federationdomain/endpoints/idpdiscovery/idp_discovery_handler_test.go @@ -41,6 +41,7 @@ func TestIDPDiscovery(t *testing.T) { WithOIDC(oidctestutil.NewTestUpstreamOIDCIdentityProviderBuilder().WithName("a-some-oidc-idp").Build()). WithLDAP(oidctestutil.NewTestUpstreamLDAPIdentityProviderBuilder().WithName("z-some-ldap-idp").Build()). WithLDAP(oidctestutil.NewTestUpstreamLDAPIdentityProviderBuilder().WithName("x-some-ldap-idp").Build()). + WithGitHub(oidctestutil.NewTestUpstreamGitHubIdentityProviderBuilder().WithName("g-some-github-idp").Build()). WithActiveDirectory(oidctestutil.NewTestUpstreamLDAPIdentityProviderBuilder().WithName("z-some-ad-idp").Build()). WithActiveDirectory(oidctestutil.NewTestUpstreamLDAPIdentityProviderBuilder().WithName("y-some-ad-idp").Build()). BuildFederationDomainIdentityProvidersListerFinder(), @@ -50,6 +51,7 @@ func TestIDPDiscovery(t *testing.T) { "pinniped_identity_providers": [ {"name": "a-some-ldap-idp", "type": "ldap", "flows": ["cli_password", "browser_authcode"]}, {"name": "a-some-oidc-idp", "type": "oidc", "flows": ["browser_authcode"]}, + {"name": "g-some-github-idp", "type": "github", "flows": ["browser_authcode"]}, {"name": "x-some-ldap-idp", "type": "ldap", "flows": ["cli_password", "browser_authcode"]}, {"name": "x-some-oidc-idp", "type": "oidc", "flows": ["browser_authcode"]}, {"name": "y-some-ad-idp", "type": "activedirectory", "flows": ["cli_password", "browser_authcode"]}, @@ -59,12 +61,14 @@ func TestIDPDiscovery(t *testing.T) { ], "pinniped_supported_identity_provider_types": [ {"type": "activedirectory"}, + {"type": "github"}, {"type": "ldap"}, {"type": "oidc"} ] }`), wantSecondResponseBodyJSON: here.Doc(`{ "pinniped_identity_providers": [ + {"name": "g-some-github-idp", "type": "github", "flows": ["browser_authcode"]}, {"name": "some-other-ad-idp-1", "type": "activedirectory", "flows": ["cli_password", "browser_authcode"]}, {"name": "some-other-ad-idp-2", "type": "activedirectory", "flows": ["cli_password", "browser_authcode"]}, {"name": "some-other-ldap-idp-1", "type": "ldap", "flows": ["cli_password", "browser_authcode"]}, @@ -74,6 +78,7 @@ func TestIDPDiscovery(t *testing.T) { ], "pinniped_supported_identity_provider_types": [ {"type": "activedirectory"}, + {"type": "github"}, {"type": "ldap"}, {"type": "oidc"} ] @@ -91,6 +96,7 @@ func TestIDPDiscovery(t *testing.T) { "pinniped_identity_providers": [], "pinniped_supported_identity_provider_types": [ {"type": "activedirectory"}, + {"type": "github"}, {"type": "ldap"}, {"type": "oidc"} ] @@ -106,6 +112,7 @@ func TestIDPDiscovery(t *testing.T) { ], "pinniped_supported_identity_provider_types": [ {"type": "activedirectory"}, + {"type": "github"}, {"type": "ldap"}, {"type": "oidc"} ] diff --git a/internal/federationdomain/endpoints/token/token_handler.go b/internal/federationdomain/endpoints/token/token_handler.go index 50f127f14..038ee01fa 100644 --- a/internal/federationdomain/endpoints/token/token_handler.go +++ b/internal/federationdomain/endpoints/token/token_handler.go @@ -233,7 +233,7 @@ func findProviderByNameAndType( idpLister federationdomainproviders.FederationDomainIdentityProvidersListerI, ) (resolvedprovider.FederationDomainResolvedIdentityProvider, error) { for _, p := range idpLister.GetIdentityProviders() { - if p.GetSessionProviderType() == providerType && p.GetProvider().GetName() == providerResourceName { + if p.GetSessionProviderType() == providerType && p.GetProvider().GetResourceName() == providerResourceName { if p.GetProvider().GetResourceUID() != mustHaveResourceUID { return nil, errorsx.WithStack(errUpstreamRefreshError().WithHint( "Provider from upstream session data has changed its resource UID since authentication.")) diff --git a/internal/federationdomain/endpoints/token/token_handler_test.go b/internal/federationdomain/endpoints/token/token_handler_test.go index 65bb4db1f..577d7442f 100644 --- a/internal/federationdomain/endpoints/token/token_handler_test.go +++ b/internal/federationdomain/endpoints/token/token_handler_test.go @@ -51,6 +51,7 @@ import ( "go.pinniped.dev/internal/federationdomain/oidc" "go.pinniped.dev/internal/federationdomain/oidcclientvalidator" "go.pinniped.dev/internal/federationdomain/storage" + "go.pinniped.dev/internal/federationdomain/upstreamprovider" "go.pinniped.dev/internal/fositestorage/accesstoken" "go.pinniped.dev/internal/fositestorage/authorizationcode" "go.pinniped.dev/internal/fositestorage/openidconnect" @@ -268,30 +269,43 @@ var ( } ) -type expectedUpstreamRefresh struct { +type expectedOIDCUpstreamRefresh struct { performedByUpstreamName string - args *oidctestutil.PerformRefreshArgs + args *oidctestutil.PerformOIDCRefreshArgs } -type expectedUpstreamValidateTokens struct { +type expectedLDAPUpstreamRefresh struct { + performedByUpstreamName string + args *oidctestutil.PerformLDAPRefreshArgs +} + +type expectedGithubUpstreamRefresh struct { + performedByUpstreamName string + args *oidctestutil.GetUserArgs +} + +type expectedOIDCUpstreamValidateTokens struct { performedByUpstreamName string args *oidctestutil.ValidateTokenAndMergeWithUserInfoArgs } type tokenEndpointResponseExpectedValues struct { - wantStatus int - wantSuccessBodyFields []string - wantErrorResponseBody string - wantClientID string - wantRequestedScopes []string - wantGrantedScopes []string - wantUsername string - wantGroups []string - wantUpstreamRefreshCall *expectedUpstreamRefresh - wantUpstreamOIDCValidateTokenCall *expectedUpstreamValidateTokens - wantCustomSessionDataStored *psession.CustomSessionData - wantWarnings []RecordedWarning - wantAdditionalClaims map[string]any + wantStatus int + wantSuccessBodyFields []string + wantErrorResponseBody string + wantClientID string + wantRequestedScopes []string + wantGrantedScopes []string + wantUsername string + wantGroups []string + wantOIDCUpstreamRefreshCall *expectedOIDCUpstreamRefresh + wantLDAPUpstreamRefreshCall *expectedLDAPUpstreamRefresh + wantActiveDirectoryUpstreamRefreshCall *expectedLDAPUpstreamRefresh + wantGithubUpstreamRefreshCall *expectedGithubUpstreamRefresh + wantUpstreamOIDCValidateTokenCall *expectedOIDCUpstreamValidateTokens + wantCustomSessionDataStored *psession.CustomSessionData + wantWarnings []RecordedWarning + wantAdditionalClaims map[string]interface{} // The expected lifetime of the ID tokens issued by authcode exchange and refresh, but not token exchange. // When zero, will assume that the test wants the default value for ID token lifetime. wantIDTokenLifetimeSeconds int @@ -1828,6 +1842,11 @@ func TestRefreshGrant(t *testing.T) { activeDirectoryUpstreamType = "activedirectory" activeDirectoryUpstreamDN = "some-ad-user-dn" + githubUpstreamName = "some-github-idp" + githubUpstreamResourceUID = "github-resource-uid" + githubUpstreamType = "github" + githubUpstreamAccessToken = "some-opaque-access-token-from-github" //nolint:gosec // this is not a credential + transformationUsernamePrefix = "username_prefix:" transformationGroupsPrefix = "groups_prefix:" ) @@ -1843,6 +1862,18 @@ func TestRefreshGrant(t *testing.T) { WithResourceUID(oidcUpstreamResourceUID) } + upstreamGitHubIdentityProviderBuilder := func() *oidctestutil.TestUpstreamGitHubIdentityProviderBuilder { + goodGitHubUser := &upstreamprovider.GitHubUser{ + Username: goodUsername, + Groups: goodGroups, + DownstreamSubject: goodSubject, + } + return oidctestutil.NewTestUpstreamGitHubIdentityProviderBuilder(). + WithName(githubUpstreamName). + WithResourceUID(githubUpstreamResourceUID). + WithUser(goodGitHubUser) + } + initialUpstreamOIDCRefreshTokenCustomSessionData := func() *psession.CustomSessionData { return &psession.CustomSessionData{ Username: goodUsername, @@ -1859,6 +1890,20 @@ func TestRefreshGrant(t *testing.T) { } } + initialUpstreamGitHubCustomSessionData := func() *psession.CustomSessionData { + return &psession.CustomSessionData{ + Username: goodUsername, + UpstreamUsername: goodUsername, + UpstreamGroups: goodGroups, + ProviderName: githubUpstreamName, + ProviderUID: githubUpstreamResourceUID, + ProviderType: githubUpstreamType, + GitHub: &psession.GitHubSessionData{ + UpstreamAccessToken: githubUpstreamAccessToken, + }, + } + } + initialUpstreamOIDCRefreshTokenCustomSessionDataWithUsername := func(downstreamUsername string) *psession.CustomSessionData { customSessionData := initialUpstreamOIDCRefreshTokenCustomSessionData() customSessionData.Username = downstreamUsername @@ -1893,42 +1938,63 @@ func TestRefreshGrant(t *testing.T) { return sessionData } - happyOIDCUpstreamRefreshCall := func() *expectedUpstreamRefresh { - return &expectedUpstreamRefresh{ + happyOIDCUpstreamRefreshCall := func() *expectedOIDCUpstreamRefresh { + return &expectedOIDCUpstreamRefresh{ performedByUpstreamName: oidcUpstreamName, - args: &oidctestutil.PerformRefreshArgs{ + args: &oidctestutil.PerformOIDCRefreshArgs{ Ctx: nil, // this will be filled in with the actual request context by the test below RefreshToken: oidcUpstreamInitialRefreshToken, }, } } - happyLDAPUpstreamRefreshCall := func() *expectedUpstreamRefresh { - return &expectedUpstreamRefresh{ + happyGitHubUpstreamRefreshCall := func() *expectedGithubUpstreamRefresh { + return &expectedGithubUpstreamRefresh{ + performedByUpstreamName: githubUpstreamName, + args: &oidctestutil.GetUserArgs{ + Ctx: nil, // this will be filled in with the actual request context by the test below + AccessToken: githubUpstreamAccessToken, + IDPDisplayName: githubUpstreamName, + }, + } + } + + happyLDAPUpstreamRefreshCall := func() *expectedLDAPUpstreamRefresh { + return &expectedLDAPUpstreamRefresh{ performedByUpstreamName: ldapUpstreamName, - args: &oidctestutil.PerformRefreshArgs{ - Ctx: nil, - DN: ldapUpstreamDN, - ExpectedSubject: goodSubject, - ExpectedUsername: goodUsername, + args: &oidctestutil.PerformLDAPRefreshArgs{ + Ctx: nil, // this will be filled in with the actual request context by the test below + StoredRefreshAttributes: upstreamprovider.LDAPRefreshAttributes{ + Username: goodUsername, + Subject: goodSubject, + DN: ldapUpstreamDN, + Groups: goodGroups, + AdditionalAttributes: nil, + }, + IDPDisplayName: ldapUpstreamName, }, } } - happyActiveDirectoryUpstreamRefreshCall := func() *expectedUpstreamRefresh { - return &expectedUpstreamRefresh{ + happyActiveDirectoryUpstreamRefreshCall := func() *expectedLDAPUpstreamRefresh { + return &expectedLDAPUpstreamRefresh{ performedByUpstreamName: activeDirectoryUpstreamName, - args: &oidctestutil.PerformRefreshArgs{ - Ctx: nil, - DN: activeDirectoryUpstreamDN, - ExpectedSubject: goodSubject, - ExpectedUsername: goodUsername, + args: &oidctestutil.PerformLDAPRefreshArgs{ + Ctx: nil, // this will be filled in with the actual request context by the test below + StoredRefreshAttributes: upstreamprovider.LDAPRefreshAttributes{ + Username: goodUsername, + Subject: goodSubject, + DN: activeDirectoryUpstreamDN, + Groups: goodGroups, + AdditionalAttributes: nil, + }, + IDPDisplayName: activeDirectoryUpstreamName, }, } } - happyUpstreamValidateTokenCall := func(expectedTokens *oauth2.Token, requireIDToken bool) *expectedUpstreamValidateTokens { - return &expectedUpstreamValidateTokens{ + happyUpstreamValidateTokenCall := func(expectedTokens *oauth2.Token, requireIDToken bool) *expectedOIDCUpstreamValidateTokens { + return &expectedOIDCUpstreamValidateTokens{ performedByUpstreamName: oidcUpstreamName, args: &oidctestutil.ValidateTokenAndMergeWithUserInfoArgs{ Ctx: nil, // this will be filled in with the actual request context by the test below @@ -1976,7 +2042,7 @@ func TestRefreshGrant(t *testing.T) { // same as the same values as the authcode exchange case. want := happyAuthcodeExchangeTokenResponseForOpenIDAndOfflineAccess(wantCustomSessionDataStored) // Should always try to perform an upstream refresh. - want.wantUpstreamRefreshCall = happyOIDCUpstreamRefreshCall() + want.wantOIDCUpstreamRefreshCall = happyOIDCUpstreamRefreshCall() if expectToValidateToken != nil { want.wantUpstreamOIDCValidateTokenCall = happyUpstreamValidateTokenCall(expectToValidateToken, true) } @@ -1988,13 +2054,22 @@ func TestRefreshGrant(t *testing.T) { // same as the same values as the authcode exchange case. want := happyAuthcodeExchangeTokenResponseForOpenIDAndOfflineAccessWithUsernameAndGroups(wantCustomSessionDataStored, wantDownstreamUsername, wantDownstreamGroups) // Should always try to perform an upstream refresh. - want.wantUpstreamRefreshCall = happyOIDCUpstreamRefreshCall() + want.wantOIDCUpstreamRefreshCall = happyOIDCUpstreamRefreshCall() if expectToValidateToken != nil { want.wantUpstreamOIDCValidateTokenCall = happyUpstreamValidateTokenCall(expectToValidateToken, true) } return want } + happyRefreshTokenResponseForGitHubAndOfflineAccessWithUsernameAndGroups := func(wantCustomSessionDataStored *psession.CustomSessionData, wantDownstreamUsername string, wantDownstreamGroups []string) tokenEndpointResponseExpectedValues { + // Should always have some custom session data stored. The other expectations happens to be the + // same as the same values as the authcode exchange case. + want := happyAuthcodeExchangeTokenResponseForOpenIDAndOfflineAccessWithUsernameAndGroups(wantCustomSessionDataStored, wantDownstreamUsername, wantDownstreamGroups) + // Should always try to perform an upstream refresh. + want.wantGithubUpstreamRefreshCall = happyGitHubUpstreamRefreshCall() + return want + } + happyRefreshTokenResponseForOpenIDAndOfflineAccessWithAdditionalClaims := func(wantCustomSessionDataStored *psession.CustomSessionData, expectToValidateToken *oauth2.Token, wantAdditionalClaims map[string]any) tokenEndpointResponseExpectedValues { want := happyRefreshTokenResponseForOpenIDAndOfflineAccess(wantCustomSessionDataStored, expectToValidateToken) want.wantAdditionalClaims = wantAdditionalClaims @@ -2003,19 +2078,19 @@ func TestRefreshGrant(t *testing.T) { happyRefreshTokenResponseForLDAP := func(wantCustomSessionDataStored *psession.CustomSessionData) tokenEndpointResponseExpectedValues { want := happyAuthcodeExchangeTokenResponseForOpenIDAndOfflineAccess(wantCustomSessionDataStored) - want.wantUpstreamRefreshCall = happyLDAPUpstreamRefreshCall() + want.wantLDAPUpstreamRefreshCall = happyLDAPUpstreamRefreshCall() return want } happyRefreshTokenResponseForLDAPWithUsernameAndGroups := func(wantCustomSessionDataStored *psession.CustomSessionData, wantDownstreamUsername string, wantDownstreamGroups []string) tokenEndpointResponseExpectedValues { want := happyAuthcodeExchangeTokenResponseForOpenIDAndOfflineAccessWithUsernameAndGroups(wantCustomSessionDataStored, wantDownstreamUsername, wantDownstreamGroups) - want.wantUpstreamRefreshCall = happyLDAPUpstreamRefreshCall() + want.wantLDAPUpstreamRefreshCall = happyLDAPUpstreamRefreshCall() return want } happyRefreshTokenResponseForActiveDirectory := func(wantCustomSessionDataStored *psession.CustomSessionData) tokenEndpointResponseExpectedValues { want := happyAuthcodeExchangeTokenResponseForOpenIDAndOfflineAccess(wantCustomSessionDataStored) - want.wantUpstreamRefreshCall = happyActiveDirectoryUpstreamRefreshCall() + want.wantActiveDirectoryUpstreamRefreshCall = happyActiveDirectoryUpstreamRefreshCall() return want } @@ -2079,6 +2154,14 @@ func TestRefreshGrant(t *testing.T) { ), } + happyAuthcodeExchangeInputsForGithubUpstream := authcodeExchangeInputs{ + modifyAuthRequest: func(r *http.Request) { r.Form.Set("scope", "openid offline_access username groups") }, + customSessionData: initialUpstreamGitHubCustomSessionData(), + want: happyAuthcodeExchangeTokenResponseForOpenIDAndOfflineAccess( + initialUpstreamGitHubCustomSessionData(), + ), + } + happyAuthcodeExchangeInputsForLDAPUpstream := authcodeExchangeInputs{ modifyAuthRequest: func(r *http.Request) { r.Form.Set("scope", "openid offline_access username groups") }, customSessionData: happyLDAPCustomSessionData, @@ -2151,6 +2234,19 @@ func TestRefreshGrant(t *testing.T) { ), }, }, + { + name: "happy path refresh grant with GitHub upstream", + idps: testidplister.NewUpstreamIDPListerBuilder().WithGitHub( + upstreamGitHubIdentityProviderBuilder().Build()), + authcodeExchange: happyAuthcodeExchangeInputsForGithubUpstream, + refreshRequest: refreshRequestInputs{ + want: happyRefreshTokenResponseForGitHubAndOfflineAccessWithUsernameAndGroups( + initialUpstreamGitHubCustomSessionData(), + goodUsername, + goodGroups, + ), + }, + }, { name: "happy path refresh grant with OIDC upstream with identity transformations which modify the username and group names when the upstream refresh does not return new username or groups then it reruns the transformations on the old upstream username and groups", idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC( @@ -2184,7 +2280,7 @@ func TestRefreshGrant(t *testing.T) { wantGrantedScopes: []string{"openid", "offline_access", "username", "groups"}, wantUsername: transformationUsernamePrefix + goodUsername, wantGroups: testutil.AddPrefixToEach(transformationGroupsPrefix, goodGroups), - wantUpstreamRefreshCall: happyOIDCUpstreamRefreshCall(), + wantOIDCUpstreamRefreshCall: happyOIDCUpstreamRefreshCall(), wantUpstreamOIDCValidateTokenCall: happyUpstreamValidateTokenCall(refreshedUpstreamTokensWithRefreshTokenWithoutIDToken(), false), wantCustomSessionDataStored: upstreamOIDCCustomSessionDataWithNewRefreshTokenWithUsername(oidcUpstreamRefreshedRefreshToken, transformationUsernamePrefix+goodUsername), }, @@ -2218,7 +2314,7 @@ func TestRefreshGrant(t *testing.T) { }, refreshRequest: refreshRequestInputs{ want: tokenEndpointResponseExpectedValues{ - wantUpstreamRefreshCall: happyOIDCUpstreamRefreshCall(), + wantOIDCUpstreamRefreshCall: happyOIDCUpstreamRefreshCall(), wantUpstreamOIDCValidateTokenCall: happyUpstreamValidateTokenCall(refreshedUpstreamTokensWithIDAndRefreshTokens(), true), wantStatus: http.StatusUnauthorized, wantErrorResponseBody: here.Doc(` @@ -2258,7 +2354,7 @@ func TestRefreshGrant(t *testing.T) { }, refreshRequest: refreshRequestInputs{ want: tokenEndpointResponseExpectedValues{ - wantUpstreamRefreshCall: happyOIDCUpstreamRefreshCall(), + wantOIDCUpstreamRefreshCall: happyOIDCUpstreamRefreshCall(), wantUpstreamOIDCValidateTokenCall: happyUpstreamValidateTokenCall(refreshedUpstreamTokensWithIDAndRefreshTokens(), true), wantStatus: http.StatusUnauthorized, wantErrorResponseBody: here.Doc(` @@ -2508,7 +2604,7 @@ func TestRefreshGrant(t *testing.T) { wantCustomSessionDataStored: upstreamOIDCCustomSessionDataWithNewRefreshToken(oidcUpstreamRefreshedRefreshToken), wantUsername: "", wantGroups: goodGroups, - wantUpstreamRefreshCall: happyOIDCUpstreamRefreshCall(), + wantOIDCUpstreamRefreshCall: happyOIDCUpstreamRefreshCall(), wantUpstreamOIDCValidateTokenCall: happyUpstreamValidateTokenCall(refreshedUpstreamTokensWithIDAndRefreshTokens(), true), }), }, @@ -2563,7 +2659,7 @@ func TestRefreshGrant(t *testing.T) { wantGrantedScopes: []string{"openid", "offline_access", "username", "groups"}, wantUsername: goodUsername, wantGroups: goodGroups, - wantUpstreamOIDCValidateTokenCall: &expectedUpstreamValidateTokens{ + wantUpstreamOIDCValidateTokenCall: &expectedOIDCUpstreamValidateTokens{ oidcUpstreamName, &oidctestutil.ValidateTokenAndMergeWithUserInfoArgs{ Ctx: nil, // this will be filled in with the actual request context by the test below @@ -2606,7 +2702,7 @@ func TestRefreshGrant(t *testing.T) { wantSuccessBodyFields: []string{"refresh_token", "access_token", "token_type", "expires_in", "scope"}, wantRequestedScopes: []string{"offline_access"}, wantGrantedScopes: []string{"offline_access", "username", "groups"}, - wantUpstreamRefreshCall: happyOIDCUpstreamRefreshCall(), + wantOIDCUpstreamRefreshCall: happyOIDCUpstreamRefreshCall(), wantUpstreamOIDCValidateTokenCall: happyUpstreamValidateTokenCall(refreshedUpstreamTokensWithRefreshTokenWithoutIDToken(), false), wantCustomSessionDataStored: upstreamOIDCCustomSessionDataWithNewRefreshToken(oidcUpstreamRefreshedRefreshToken), wantUsername: goodUsername, @@ -2632,7 +2728,7 @@ func TestRefreshGrant(t *testing.T) { wantGrantedScopes: []string{"openid", "offline_access", "username", "groups"}, wantUsername: goodUsername, wantGroups: goodGroups, - wantUpstreamRefreshCall: happyOIDCUpstreamRefreshCall(), + wantOIDCUpstreamRefreshCall: happyOIDCUpstreamRefreshCall(), wantUpstreamOIDCValidateTokenCall: happyUpstreamValidateTokenCall(refreshedUpstreamTokensWithRefreshTokenWithoutIDToken(), false), wantCustomSessionDataStored: upstreamOIDCCustomSessionDataWithNewRefreshToken(oidcUpstreamRefreshedRefreshToken), }, @@ -2659,7 +2755,7 @@ func TestRefreshGrant(t *testing.T) { wantGrantedScopes: []string{"openid", "offline_access", "username", "groups"}, wantUsername: goodUsername, wantGroups: []string{"new-group1", "new-group2", "new-group3"}, - wantUpstreamRefreshCall: happyOIDCUpstreamRefreshCall(), + wantOIDCUpstreamRefreshCall: happyOIDCUpstreamRefreshCall(), wantUpstreamOIDCValidateTokenCall: happyUpstreamValidateTokenCall(refreshedUpstreamTokensWithIDAndRefreshTokens(), true), wantCustomSessionDataStored: upstreamOIDCCustomSessionDataWithNewRefreshToken(oidcUpstreamRefreshedRefreshToken), wantWarnings: []RecordedWarning{ @@ -2700,7 +2796,7 @@ func TestRefreshGrant(t *testing.T) { wantGrantedScopes: []string{"openid", "offline_access", "username", "groups"}, wantUsername: goodUsername, wantGroups: []string{"new-group1", "new-group2", "new-group3"}, - wantUpstreamRefreshCall: happyOIDCUpstreamRefreshCall(), + wantOIDCUpstreamRefreshCall: happyOIDCUpstreamRefreshCall(), wantUpstreamOIDCValidateTokenCall: happyUpstreamValidateTokenCall(refreshedUpstreamTokensWithIDAndRefreshTokens(), true), wantCustomSessionDataStored: upstreamOIDCCustomSessionDataWithNewRefreshToken(oidcUpstreamRefreshedRefreshToken), wantWarnings: nil, // dynamic clients should not get these warnings which are intended for the pinniped-cli client @@ -2728,7 +2824,7 @@ func TestRefreshGrant(t *testing.T) { wantGrantedScopes: []string{"openid", "offline_access", "username", "groups"}, wantUsername: goodUsername, wantGroups: []string{"new-group1", "new-group2", "new-group3"}, - wantUpstreamRefreshCall: happyOIDCUpstreamRefreshCall(), + wantOIDCUpstreamRefreshCall: happyOIDCUpstreamRefreshCall(), wantUpstreamOIDCValidateTokenCall: happyUpstreamValidateTokenCall(refreshedUpstreamTokensWithIDAndRefreshTokens(), true), wantCustomSessionDataStored: upstreamOIDCCustomSessionDataWithNewRefreshToken(oidcUpstreamRefreshedRefreshToken), wantWarnings: []RecordedWarning{ @@ -2759,7 +2855,7 @@ func TestRefreshGrant(t *testing.T) { wantGrantedScopes: []string{"openid", "offline_access", "username", "groups"}, wantUsername: goodUsername, wantGroups: []string{}, // the user no longer belongs to any groups - wantUpstreamRefreshCall: happyOIDCUpstreamRefreshCall(), + wantOIDCUpstreamRefreshCall: happyOIDCUpstreamRefreshCall(), wantUpstreamOIDCValidateTokenCall: happyUpstreamValidateTokenCall(refreshedUpstreamTokensWithIDAndRefreshTokens(), true), wantCustomSessionDataStored: upstreamOIDCCustomSessionDataWithNewRefreshToken(oidcUpstreamRefreshedRefreshToken), wantWarnings: []RecordedWarning{ @@ -2789,7 +2885,7 @@ func TestRefreshGrant(t *testing.T) { wantGrantedScopes: []string{"openid", "offline_access", "username", "groups"}, wantUsername: goodUsername, wantGroups: goodGroups, // the same groups as from the initial login - wantUpstreamRefreshCall: happyOIDCUpstreamRefreshCall(), + wantOIDCUpstreamRefreshCall: happyOIDCUpstreamRefreshCall(), wantUpstreamOIDCValidateTokenCall: happyUpstreamValidateTokenCall(refreshedUpstreamTokensWithIDAndRefreshTokens(), true), wantCustomSessionDataStored: upstreamOIDCCustomSessionDataWithNewRefreshToken(oidcUpstreamRefreshedRefreshToken), }, @@ -2814,7 +2910,7 @@ func TestRefreshGrant(t *testing.T) { wantGrantedScopes: []string{"openid", "offline_access", "username", "groups"}, wantUsername: goodUsername, wantGroups: []string{"new-group1", "new-group2", "new-group3"}, - wantUpstreamRefreshCall: happyLDAPUpstreamRefreshCall(), + wantLDAPUpstreamRefreshCall: happyLDAPUpstreamRefreshCall(), wantCustomSessionDataStored: happyLDAPCustomSessionData, wantWarnings: []RecordedWarning{ {Text: `User "some-username" has been added to the following groups: ["new-group1" "new-group2" "new-group3"]`}, @@ -2852,12 +2948,79 @@ func TestRefreshGrant(t *testing.T) { wantGrantedScopes: []string{"openid", "offline_access", "username", "groups"}, wantUsername: goodUsername, wantGroups: []string{"new-group1", "new-group2", "new-group3"}, - wantUpstreamRefreshCall: happyLDAPUpstreamRefreshCall(), + wantLDAPUpstreamRefreshCall: happyLDAPUpstreamRefreshCall(), wantCustomSessionDataStored: happyLDAPCustomSessionData, wantWarnings: nil, // dynamic clients should not get these warnings which are intended for the pinniped-cli client }, }, }, + { + name: "happy path refresh grant when the upstream refresh returns new group memberships from GitHub, it updates groups", + idps: testidplister.NewUpstreamIDPListerBuilder().WithGitHub(oidctestutil.NewTestUpstreamGitHubIdentityProviderBuilder(). + WithName(githubUpstreamName). + WithResourceUID(githubUpstreamResourceUID). + WithUser(&upstreamprovider.GitHubUser{ + Username: goodUsername, + Groups: []string{goodGroups[0], "new-group1", "new-group2", "new-group3"}, + DownstreamSubject: goodSubject, + }).Build(), + ), + authcodeExchange: happyAuthcodeExchangeInputsForGithubUpstream, + refreshRequest: refreshRequestInputs{ + want: tokenEndpointResponseExpectedValues{ + wantStatus: http.StatusOK, + wantClientID: pinnipedCLIClientID, + wantSuccessBodyFields: []string{"refresh_token", "access_token", "id_token", "token_type", "expires_in", "scope"}, + wantRequestedScopes: []string{"openid", "offline_access", "username", "groups"}, + wantGrantedScopes: []string{"openid", "offline_access", "username", "groups"}, + wantUsername: goodUsername, + wantGroups: []string{goodGroups[0], "new-group1", "new-group2", "new-group3"}, + wantGithubUpstreamRefreshCall: happyGitHubUpstreamRefreshCall(), + wantCustomSessionDataStored: initialUpstreamGitHubCustomSessionData(), + wantWarnings: []RecordedWarning{ + {Text: `User "some-username" has been added to the following groups: ["new-group1" "new-group2" "new-group3"]`}, + {Text: `User "some-username" has been removed from the following groups: ["groups2"]`}, + }, + }, + }, + }, + { + name: "happy path refresh grant when the upstream refresh returns new group memberships from GitHub, it updates groups, using dynamic client - updates groups without outputting warnings", + idps: testidplister.NewUpstreamIDPListerBuilder().WithGitHub(oidctestutil.NewTestUpstreamGitHubIdentityProviderBuilder(). + WithName(githubUpstreamName). + WithResourceUID(githubUpstreamResourceUID). + WithUser(&upstreamprovider.GitHubUser{ + Username: goodUsername, + Groups: []string{goodGroups[0], "new-group1", "new-group2", "new-group3"}, + DownstreamSubject: goodSubject, + }).Build(), + ), + kubeResources: addFullyCapableDynamicClientAndSecretToKubeResources, + authcodeExchange: authcodeExchangeInputs{ + customSessionData: initialUpstreamGitHubCustomSessionData(), + modifyAuthRequest: func(r *http.Request) { + addDynamicClientIDToFormPostBody(r) + r.Form.Set("scope", "openid offline_access username groups") + }, + modifyTokenRequest: modifyAuthcodeTokenRequestWithDynamicClientAuth, + want: withWantDynamicClientID(happyAuthcodeExchangeTokenResponseForOpenIDAndOfflineAccess(initialUpstreamGitHubCustomSessionData())), + }, + refreshRequest: refreshRequestInputs{ + modifyTokenRequest: modifyRefreshTokenRequestWithDynamicClientAuth, + want: tokenEndpointResponseExpectedValues{ + wantStatus: http.StatusOK, + wantClientID: dynamicClientID, + wantSuccessBodyFields: []string{"refresh_token", "access_token", "id_token", "token_type", "expires_in", "scope"}, + wantRequestedScopes: []string{"openid", "offline_access", "username", "groups"}, + wantGrantedScopes: []string{"openid", "offline_access", "username", "groups"}, + wantUsername: goodUsername, + wantGroups: []string{goodGroups[0], "new-group1", "new-group2", "new-group3"}, + wantGithubUpstreamRefreshCall: happyGitHubUpstreamRefreshCall(), + wantCustomSessionDataStored: initialUpstreamGitHubCustomSessionData(), + wantWarnings: nil, // dynamic clients should not get these warnings which are intended for the pinniped-cli client + }, + }, + }, { name: "happy path refresh grant when the upstream refresh returns empty list of group memberships from LDAP, it updates groups to an empty list", idps: testidplister.NewUpstreamIDPListerBuilder().WithLDAP(oidctestutil.NewTestUpstreamLDAPIdentityProviderBuilder(). @@ -2877,7 +3040,7 @@ func TestRefreshGrant(t *testing.T) { wantGrantedScopes: []string{"openid", "offline_access", "username", "groups"}, wantUsername: goodUsername, wantGroups: []string{}, - wantUpstreamRefreshCall: happyLDAPUpstreamRefreshCall(), + wantLDAPUpstreamRefreshCall: happyLDAPUpstreamRefreshCall(), wantCustomSessionDataStored: happyLDAPCustomSessionData, wantWarnings: []RecordedWarning{ {Text: `User "some-username" has been removed from the following groups: ["group1" "groups2"]`}, @@ -2920,7 +3083,7 @@ func TestRefreshGrant(t *testing.T) { wantGrantedScopes: []string{"openid", "offline_access", "username", "groups"}, // username and groups were not requested, but granted anyway for backwards compatibility wantUsername: goodUsername, wantGroups: []string{"new-group1", "new-group2", "new-group3"}, - wantUpstreamRefreshCall: happyLDAPUpstreamRefreshCall(), + wantLDAPUpstreamRefreshCall: happyLDAPUpstreamRefreshCall(), wantCustomSessionDataStored: happyLDAPCustomSessionData, wantWarnings: []RecordedWarning{ {Text: `User "some-username" has been added to the following groups: ["new-group1" "new-group2" "new-group3"]`}, @@ -2966,7 +3129,7 @@ func TestRefreshGrant(t *testing.T) { wantGrantedScopes: []string{"openid", "offline_access", "username", "groups"}, // username and groups were not requested, but granted anyway for backwards compatibility wantUsername: goodUsername, wantGroups: []string{"new-group1", "new-group2", "new-group3"}, - wantUpstreamRefreshCall: happyOIDCUpstreamRefreshCall(), + wantOIDCUpstreamRefreshCall: happyOIDCUpstreamRefreshCall(), wantUpstreamOIDCValidateTokenCall: happyUpstreamValidateTokenCall(refreshedUpstreamTokensWithIDAndRefreshTokens(), true), wantCustomSessionDataStored: upstreamOIDCCustomSessionDataWithNewRefreshToken(oidcUpstreamRefreshedRefreshToken), wantWarnings: []RecordedWarning{ @@ -3019,7 +3182,7 @@ func TestRefreshGrant(t *testing.T) { wantGrantedScopes: []string{"openid", "offline_access", "username"}, wantUsername: goodUsername, wantGroups: nil, - wantUpstreamRefreshCall: happyOIDCUpstreamRefreshCall(), + wantOIDCUpstreamRefreshCall: happyOIDCUpstreamRefreshCall(), wantUpstreamOIDCValidateTokenCall: happyUpstreamValidateTokenCall(refreshedUpstreamTokensWithIDAndRefreshTokens(), true), wantCustomSessionDataStored: upstreamOIDCCustomSessionDataWithNewRefreshToken(oidcUpstreamRefreshedRefreshToken), }, @@ -3070,7 +3233,7 @@ func TestRefreshGrant(t *testing.T) { r.SetBasicAuth(dynamicClientID, testutil.PlaintextPassword1) // Use basic auth header instead. }, want: tokenEndpointResponseExpectedValues{ - wantUpstreamRefreshCall: happyOIDCUpstreamRefreshCall(), + wantOIDCUpstreamRefreshCall: happyOIDCUpstreamRefreshCall(), wantUpstreamOIDCValidateTokenCall: happyUpstreamValidateTokenCall(refreshedUpstreamTokensWithIDAndRefreshTokens(), true), wantStatus: http.StatusUnauthorized, // auth was rejected because of the upstream group to which the user belonged, as shown by the configured RejectedAuthenticationMessage appearing here @@ -3120,7 +3283,7 @@ func TestRefreshGrant(t *testing.T) { wantGrantedScopes: []string{"openid", "offline_access", "username", "groups"}, wantUsername: goodUsername, wantGroups: []string{"new-group1", "new-group2", "new-group3"}, // groups are updated even though the scope was not included - wantUpstreamRefreshCall: happyLDAPUpstreamRefreshCall(), + wantLDAPUpstreamRefreshCall: happyLDAPUpstreamRefreshCall(), wantCustomSessionDataStored: happyLDAPCustomSessionData, wantWarnings: []RecordedWarning{ {Text: `User "some-username" has been added to the following groups: ["new-group1" "new-group2" "new-group3"]`}, @@ -3145,7 +3308,7 @@ func TestRefreshGrant(t *testing.T) { want: tokenEndpointResponseExpectedValues{ wantStatus: http.StatusUnauthorized, wantErrorResponseBody: fositeUpstreamGroupClaimErrorBody, - wantUpstreamRefreshCall: happyOIDCUpstreamRefreshCall(), + wantOIDCUpstreamRefreshCall: happyOIDCUpstreamRefreshCall(), wantUpstreamOIDCValidateTokenCall: happyUpstreamValidateTokenCall(refreshedUpstreamTokensWithIDAndRefreshTokens(), true), }, }, @@ -3227,7 +3390,7 @@ func TestRefreshGrant(t *testing.T) { wantGrantedScopes: []string{"openid", "offline_access", "pinniped:request-audience", "username", "groups"}, wantUsername: goodUsername, wantGroups: goodGroups, - wantUpstreamRefreshCall: happyOIDCUpstreamRefreshCall(), + wantOIDCUpstreamRefreshCall: happyOIDCUpstreamRefreshCall(), wantUpstreamOIDCValidateTokenCall: happyUpstreamValidateTokenCall(refreshedUpstreamTokensWithIDAndRefreshTokens(), true), wantCustomSessionDataStored: upstreamOIDCCustomSessionDataWithNewRefreshToken(oidcUpstreamRefreshedRefreshToken), }, @@ -3719,14 +3882,32 @@ func TestRefreshGrant(t *testing.T) { }, }, { - name: "when the upstream refresh fails during the refresh request", + name: "when the upstream refresh fails during the refresh request using OIDC upstream", idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProviderBuilder(). WithPerformRefreshError(errors.New("some upstream refresh error")).Build()), authcodeExchange: happyAuthcodeExchangeInputsForOIDCUpstream, refreshRequest: refreshRequestInputs{ want: tokenEndpointResponseExpectedValues{ - wantUpstreamRefreshCall: happyOIDCUpstreamRefreshCall(), - wantStatus: http.StatusUnauthorized, + wantOIDCUpstreamRefreshCall: happyOIDCUpstreamRefreshCall(), + wantStatus: http.StatusUnauthorized, + wantErrorResponseBody: here.Doc(` + { + "error": "error", + "error_description": "Error during upstream refresh. Upstream refresh failed." + } + `), + }, + }, + }, + { + name: "when the upstream refresh fails during the refresh request using GitHub upstream", + idps: testidplister.NewUpstreamIDPListerBuilder().WithGitHub(upstreamGitHubIdentityProviderBuilder(). + WithGetUserError(errors.New("some upstream refresh error")).Build()), + authcodeExchange: happyAuthcodeExchangeInputsForGithubUpstream, + refreshRequest: refreshRequestInputs{ + want: tokenEndpointResponseExpectedValues{ + wantGithubUpstreamRefreshCall: happyGitHubUpstreamRefreshCall(), + wantStatus: http.StatusUnauthorized, wantErrorResponseBody: here.Doc(` { "error": "error", @@ -3746,7 +3927,7 @@ func TestRefreshGrant(t *testing.T) { authcodeExchange: happyAuthcodeExchangeInputsForOIDCUpstream, refreshRequest: refreshRequestInputs{ want: tokenEndpointResponseExpectedValues{ - wantUpstreamRefreshCall: happyOIDCUpstreamRefreshCall(), + wantOIDCUpstreamRefreshCall: happyOIDCUpstreamRefreshCall(), wantUpstreamOIDCValidateTokenCall: happyUpstreamValidateTokenCall(refreshedUpstreamTokensWithIDAndRefreshTokens(), true), wantStatus: http.StatusUnauthorized, wantErrorResponseBody: here.Doc(` @@ -3774,7 +3955,7 @@ func TestRefreshGrant(t *testing.T) { authcodeExchange: happyAuthcodeExchangeInputsForOIDCUpstream, refreshRequest: refreshRequestInputs{ want: tokenEndpointResponseExpectedValues{ - wantUpstreamRefreshCall: happyOIDCUpstreamRefreshCall(), + wantOIDCUpstreamRefreshCall: happyOIDCUpstreamRefreshCall(), wantUpstreamOIDCValidateTokenCall: happyUpstreamValidateTokenCall(refreshedUpstreamTokensWithIDAndRefreshTokens(), true), wantStatus: http.StatusUnauthorized, wantErrorResponseBody: here.Doc(` @@ -3799,7 +3980,7 @@ func TestRefreshGrant(t *testing.T) { authcodeExchange: happyAuthcodeExchangeInputsForOIDCUpstream, refreshRequest: refreshRequestInputs{ want: tokenEndpointResponseExpectedValues{ - wantUpstreamRefreshCall: happyOIDCUpstreamRefreshCall(), + wantOIDCUpstreamRefreshCall: happyOIDCUpstreamRefreshCall(), wantUpstreamOIDCValidateTokenCall: happyUpstreamValidateTokenCall(refreshedUpstreamTokensWithIDAndRefreshTokens(), true), wantStatus: http.StatusUnauthorized, wantErrorResponseBody: here.Doc(` @@ -3826,7 +4007,7 @@ func TestRefreshGrant(t *testing.T) { authcodeExchange: happyAuthcodeExchangeInputsForOIDCUpstream, refreshRequest: refreshRequestInputs{ want: tokenEndpointResponseExpectedValues{ - wantUpstreamRefreshCall: happyOIDCUpstreamRefreshCall(), + wantOIDCUpstreamRefreshCall: happyOIDCUpstreamRefreshCall(), wantUpstreamOIDCValidateTokenCall: happyUpstreamValidateTokenCall(refreshedUpstreamTokensWithIDAndRefreshTokens(), true), wantStatus: http.StatusUnauthorized, wantErrorResponseBody: here.Doc(` @@ -3853,7 +4034,7 @@ func TestRefreshGrant(t *testing.T) { authcodeExchange: happyAuthcodeExchangeInputsForOIDCUpstream, refreshRequest: refreshRequestInputs{ want: tokenEndpointResponseExpectedValues{ - wantUpstreamRefreshCall: happyOIDCUpstreamRefreshCall(), + wantOIDCUpstreamRefreshCall: happyOIDCUpstreamRefreshCall(), wantUpstreamOIDCValidateTokenCall: happyUpstreamValidateTokenCall(refreshedUpstreamTokensWithIDAndRefreshTokens(), true), wantStatus: http.StatusUnauthorized, wantErrorResponseBody: here.Doc(` @@ -3943,8 +4124,8 @@ func TestRefreshGrant(t *testing.T) { }, refreshRequest: refreshRequestInputs{ want: tokenEndpointResponseExpectedValues{ - wantUpstreamRefreshCall: happyLDAPUpstreamRefreshCall(), - wantStatus: http.StatusUnauthorized, + wantLDAPUpstreamRefreshCall: happyLDAPUpstreamRefreshCall(), + wantStatus: http.StatusUnauthorized, wantErrorResponseBody: here.Doc(` { "error": "error", @@ -3983,8 +4164,8 @@ func TestRefreshGrant(t *testing.T) { }, refreshRequest: refreshRequestInputs{ want: tokenEndpointResponseExpectedValues{ - wantUpstreamRefreshCall: happyLDAPUpstreamRefreshCall(), - wantStatus: http.StatusUnauthorized, + wantLDAPUpstreamRefreshCall: happyLDAPUpstreamRefreshCall(), + wantStatus: http.StatusUnauthorized, wantErrorResponseBody: here.Doc(` { "error": "error", @@ -4057,7 +4238,7 @@ func TestRefreshGrant(t *testing.T) { wantCustomSessionDataStored: happyLDAPCustomSessionData, wantUsername: "", wantGroups: goodGroups, - wantUpstreamRefreshCall: happyLDAPUpstreamRefreshCall(), + wantLDAPUpstreamRefreshCall: happyLDAPUpstreamRefreshCall(), }), }, }, @@ -4251,8 +4432,8 @@ func TestRefreshGrant(t *testing.T) { authcodeExchange: happyAuthcodeExchangeInputsForLDAPUpstream, refreshRequest: refreshRequestInputs{ want: tokenEndpointResponseExpectedValues{ - wantUpstreamRefreshCall: happyLDAPUpstreamRefreshCall(), - wantStatus: http.StatusUnauthorized, + wantLDAPUpstreamRefreshCall: happyLDAPUpstreamRefreshCall(), + wantStatus: http.StatusUnauthorized, wantErrorResponseBody: here.Doc(` { "error": "error", @@ -4280,8 +4461,8 @@ func TestRefreshGrant(t *testing.T) { }, refreshRequest: refreshRequestInputs{ want: tokenEndpointResponseExpectedValues{ - wantUpstreamRefreshCall: happyActiveDirectoryUpstreamRefreshCall(), - wantStatus: http.StatusUnauthorized, + wantActiveDirectoryUpstreamRefreshCall: happyActiveDirectoryUpstreamRefreshCall(), + wantStatus: http.StatusUnauthorized, wantErrorResponseBody: here.Doc(` { "error": "error", @@ -4534,7 +4715,7 @@ func TestRefreshGrant(t *testing.T) { // Performing an authcode exchange should not have caused any upstream refresh, which should only // happen during a downstream refresh. - test.idps.RequireExactlyZeroCallsToPerformRefresh(t) + test.idps.RequireExactlyZeroCallsToAnyUpstreamRefresh(t) test.idps.RequireExactlyZeroCallsToValidateToken(t) // Wait one second before performing the refresh so we can see that the refreshed ID token has new issued @@ -4566,19 +4747,38 @@ func TestRefreshGrant(t *testing.T) { t.Logf("second response: %#v", refreshResponse) t.Logf("second response body: %q", refreshResponse.Body.String()) - // Test that we did or did not make a call to the upstream OIDC provider interface to perform a token refresh. - if test.refreshRequest.want.wantUpstreamRefreshCall != nil { - test.refreshRequest.want.wantUpstreamRefreshCall.args.Ctx = reqContext - test.idps.RequireExactlyOneCallToPerformRefresh(t, - test.refreshRequest.want.wantUpstreamRefreshCall.performedByUpstreamName, - test.refreshRequest.want.wantUpstreamRefreshCall.args, + // Test that we did or did not make a call to the upstream provider's interface to perform refresh. + switch { + case test.refreshRequest.want.wantOIDCUpstreamRefreshCall != nil: + test.refreshRequest.want.wantOIDCUpstreamRefreshCall.args.Ctx = reqContext + test.idps.RequireExactlyOneCallToOIDCPerformRefresh(t, + test.refreshRequest.want.wantOIDCUpstreamRefreshCall.performedByUpstreamName, + test.refreshRequest.want.wantOIDCUpstreamRefreshCall.args, ) - } else { - test.idps.RequireExactlyZeroCallsToPerformRefresh(t) + case test.refreshRequest.want.wantLDAPUpstreamRefreshCall != nil: + test.refreshRequest.want.wantLDAPUpstreamRefreshCall.args.Ctx = reqContext + test.idps.RequireExactlyOneCallToLDAPPerformRefresh(t, + test.refreshRequest.want.wantLDAPUpstreamRefreshCall.performedByUpstreamName, + test.refreshRequest.want.wantLDAPUpstreamRefreshCall.args, + ) + case test.refreshRequest.want.wantActiveDirectoryUpstreamRefreshCall != nil: + test.refreshRequest.want.wantActiveDirectoryUpstreamRefreshCall.args.Ctx = reqContext + test.idps.RequireExactlyOneCallToActiveDirectoryPerformRefresh(t, + test.refreshRequest.want.wantActiveDirectoryUpstreamRefreshCall.performedByUpstreamName, + test.refreshRequest.want.wantActiveDirectoryUpstreamRefreshCall.args, + ) + case test.refreshRequest.want.wantGithubUpstreamRefreshCall != nil: + test.refreshRequest.want.wantGithubUpstreamRefreshCall.args.Ctx = reqContext + test.idps.RequireExactlyOneCallToGithubGetUser(t, + test.refreshRequest.want.wantGithubUpstreamRefreshCall.performedByUpstreamName, + test.refreshRequest.want.wantGithubUpstreamRefreshCall.args, + ) + default: + test.idps.RequireExactlyZeroCallsToAnyUpstreamRefresh(t) } // Test that we did or did not make a call to the upstream OIDC provider interface to validate the - // new ID token that was returned by the upstream refresh. + // new ID token that was returned by the upstream refresh, in the case of an OIDC upstream. if test.refreshRequest.want.wantUpstreamOIDCValidateTokenCall != nil { test.refreshRequest.want.wantUpstreamOIDCValidateTokenCall.args.Ctx = reqContext test.idps.RequireExactlyOneCallToValidateToken(t, diff --git a/internal/federationdomain/endpointsmanager/manager_test.go b/internal/federationdomain/endpointsmanager/manager_test.go index 794602fcf..6707a9d8c 100644 --- a/internal/federationdomain/endpointsmanager/manager_test.go +++ b/internal/federationdomain/endpointsmanager/manager_test.go @@ -123,6 +123,7 @@ func TestManager(t *testing.T) { "pinniped_identity_providers": [%s], "pinniped_supported_identity_provider_types": [ {"type":"activedirectory"}, + {"type":"github"}, {"type":"ldap"}, {"type":"oidc"} ] diff --git a/internal/federationdomain/federationdomainproviders/federation_domain_identity_providers_lister_finder.go b/internal/federationdomain/federationdomainproviders/federation_domain_identity_providers_lister_finder.go index 358d57921..2e4aa887b 100644 --- a/internal/federationdomain/federationdomainproviders/federation_domain_identity_providers_lister_finder.go +++ b/internal/federationdomain/federationdomainproviders/federation_domain_identity_providers_lister_finder.go @@ -11,6 +11,7 @@ import ( "go.pinniped.dev/internal/federationdomain/idplister" "go.pinniped.dev/internal/federationdomain/resolvedprovider" + "go.pinniped.dev/internal/federationdomain/resolvedprovider/resolvedgithub" "go.pinniped.dev/internal/federationdomain/resolvedprovider/resolvedldap" "go.pinniped.dev/internal/federationdomain/resolvedprovider/resolvedoidc" "go.pinniped.dev/internal/idtransform" @@ -144,6 +145,7 @@ func (u *FederationDomainIdentityProvidersListerFinder) GetIdentityProviders() [ cachedOIDCProviders := u.wrappedLister.GetOIDCIdentityProviders() cachedLDAPProviders := u.wrappedLister.GetLDAPIdentityProviders() cachedADProviders := u.wrappedLister.GetActiveDirectoryIdentityProviders() + cachedGitHubProviders := u.wrappedLister.GetGitHubIdentityProviders() providers := []resolvedprovider.FederationDomainResolvedIdentityProvider{} // Every configured identityProvider on the FederationDomain uses an objetRef to an underlying IDP CR that might // be available as a provider in the wrapped cache. For each configured identityProvider/displayName... @@ -184,6 +186,16 @@ func (u *FederationDomainIdentityProvidersListerFinder) GetIdentityProviders() [ }) } } + for _, p := range cachedGitHubProviders { + if idp.UID == p.GetResourceUID() { + providers = append(providers, &resolvedgithub.FederationDomainResolvedGitHubIdentityProvider{ + DisplayName: idp.DisplayName, + Provider: p, + SessionProviderType: psession.ProviderTypeGitHub, + Transforms: idp.Transforms, + }) + } + } } return providers } diff --git a/internal/federationdomain/federationdomainproviders/federation_domain_identity_providers_lister_finder_test.go b/internal/federationdomain/federationdomainproviders/federation_domain_identity_providers_lister_finder_test.go index f220b9108..ef64fe62c 100644 --- a/internal/federationdomain/federationdomainproviders/federation_domain_identity_providers_lister_finder_test.go +++ b/internal/federationdomain/federationdomainproviders/federation_domain_identity_providers_lister_finder_test.go @@ -10,6 +10,7 @@ import ( "go.pinniped.dev/internal/federationdomain/idplister" "go.pinniped.dev/internal/federationdomain/resolvedprovider" + "go.pinniped.dev/internal/federationdomain/resolvedprovider/resolvedgithub" "go.pinniped.dev/internal/federationdomain/resolvedprovider/resolvedldap" "go.pinniped.dev/internal/federationdomain/resolvedprovider/resolvedoidc" "go.pinniped.dev/internal/testutil/oidctestutil" @@ -52,6 +53,14 @@ func TestFederationDomainIdentityProvidersListerFinder(t *testing.T) { WithName("my-ad-idp2"). WithResourceUID("my-ad-uid-idp2"). Build() + myDefaultGitHubIDP := oidctestutil.NewTestUpstreamGitHubIdentityProviderBuilder(). + WithName("my-default-github-idp"). + WithResourceUID("my-default-github-uid-idp"). + Build() + myGitHubIDP1 := oidctestutil.NewTestUpstreamGitHubIdentityProviderBuilder(). + WithName("my-github-idp1"). + WithResourceUID("my-github-uid-idp1"). + Build() // FederationDomainIssuers fakeIssuerURL := "https://www.fakeissuerurl.com" @@ -77,13 +86,20 @@ func TestFederationDomainIdentityProvidersListerFinder(t *testing.T) { }) require.NoError(t, err) - fdIssuerWithOIDCAndLDAPAndADIDPs, err := NewFederationDomainIssuer(fakeIssuerURL, []*FederationDomainIdentityProvider{ + fdIssuerWithDefaultGitHubIDP, err := NewFederationDomainIssuerWithDefaultIDP(fakeIssuerURL, &FederationDomainIdentityProvider{ + DisplayName: "my-default-github-idp", + UID: "my-default-github-uid-idp", + }) + require.NoError(t, err) + + fdIssuerWithOIDCAndLDAPAndADAndGitHubIDPs, err := NewFederationDomainIssuer(fakeIssuerURL, []*FederationDomainIdentityProvider{ {DisplayName: "my-oidc-idp1", UID: "my-oidc-uid-idp1"}, {DisplayName: "my-oidc-idp2", UID: "my-oidc-uid-idp2"}, {DisplayName: "my-ldap-idp1", UID: "my-ldap-uid-idp1"}, {DisplayName: "my-ldap-idp2", UID: "my-ldap-uid-idp2"}, {DisplayName: "my-ad-idp1", UID: "my-ad-uid-idp1"}, {DisplayName: "my-ad-idp2", UID: "my-ad-uid-idp2"}, + {DisplayName: "my-github-idp1", UID: "my-github-uid-idp1"}, }) require.NoError(t, err) @@ -99,6 +115,7 @@ func TestFederationDomainIdentityProvidersListerFinder(t *testing.T) { {DisplayName: "my-ldap-idp4", UID: "my-ldap-uid-idp4"}, {DisplayName: "my-ad-idp2", UID: "my-ad-uid-idp2"}, {DisplayName: "my-ad-idp3", UID: "my-ad-uid-idp3"}, + {DisplayName: "my-github-idp1", UID: "my-github-uid-idp1"}, }) require.NoError(t, err) @@ -133,6 +150,11 @@ func TestFederationDomainIdentityProvidersListerFinder(t *testing.T) { Provider: myADIDP1, SessionProviderType: "activedirectory", } + myGitHub1Resolved := &resolvedgithub.FederationDomainResolvedGitHubIdentityProvider{ + DisplayName: "my-github-idp1", + Provider: myGitHubIDP1, + SessionProviderType: "github", + } myDefaultOIDCIDPResolved := &resolvedoidc.FederationDomainResolvedOIDCIdentityProvider{ DisplayName: "my-default-oidc-idp", @@ -144,15 +166,21 @@ func TestFederationDomainIdentityProvidersListerFinder(t *testing.T) { Provider: myDefaultLDAPIDP, SessionProviderType: "ldap", } + myDefaultGitHubIDPResolved := &resolvedgithub.FederationDomainResolvedGitHubIdentityProvider{ + DisplayName: "my-default-github-idp", + Provider: myDefaultGitHubIDP, + SessionProviderType: "github", + } testFindUpstreamIDPByDisplayName := []struct { - name string - wrappedLister idplister.UpstreamIdentityProvidersLister - federationDomainIssuer *FederationDomainIssuer - findIDPByDisplayName string - wantOIDCIDPByDisplayName *resolvedoidc.FederationDomainResolvedOIDCIdentityProvider - wantLDAPIDPByDisplayName *resolvedldap.FederationDomainResolvedLDAPIdentityProvider - wantError string + name string + wrappedLister idplister.UpstreamIdentityProvidersLister + federationDomainIssuer *FederationDomainIssuer + findIDPByDisplayName string + wantOIDCIDPByDisplayName *resolvedoidc.FederationDomainResolvedOIDCIdentityProvider + wantLDAPIDPByDisplayName *resolvedldap.FederationDomainResolvedLDAPIdentityProvider + wantGitHubIDPByDisplayName *resolvedgithub.FederationDomainResolvedGitHubIdentityProvider + wantError string }{ { name: "FindUpstreamIDPByDisplayName will find an upstream IdP by display name with one IDP configured", @@ -182,8 +210,9 @@ func TestFederationDomainIdentityProvidersListerFinder(t *testing.T) { WithOIDC(myOIDCIDP2). WithLDAP(myLDAPIDP1). WithLDAP(myLDAPIDP2). + WithGitHub(myGitHubIDP1). BuildDynamicUpstreamIDPProvider(), - federationDomainIssuer: fdIssuerWithOIDCAndLDAPAndADIDPs, + federationDomainIssuer: fdIssuerWithOIDCAndLDAPAndADAndGitHubIDPs, wantOIDCIDPByDisplayName: myOIDCIDP1Resolved, }, { @@ -195,6 +224,7 @@ func TestFederationDomainIdentityProvidersListerFinder(t *testing.T) { WithLDAP(myLDAPIDP1). WithLDAP(myLDAPIDP2). WithActiveDirectory(myADIDP1). + WithGitHub(myGitHubIDP1). BuildDynamicUpstreamIDPProvider(), federationDomainIssuer: fdIssuerWithOIDCIDP1, wantOIDCIDPByDisplayName: myOIDCIDP1Resolved, @@ -208,11 +238,13 @@ func TestFederationDomainIdentityProvidersListerFinder(t *testing.T) { WithLDAP(myLDAPIDP1). WithLDAP(myLDAPIDP2). WithActiveDirectory(myADIDP1). + WithGitHub(myGitHubIDP1). BuildDynamicUpstreamIDPProvider(), - federationDomainIssuer: fdIssuerWithOIDCAndLDAPAndADIDPs, + federationDomainIssuer: fdIssuerWithOIDCAndLDAPAndADAndGitHubIDPs, + wantLDAPIDPByDisplayName: myLDAPIDP1Resolved, }, { - name: "FindUpstreamIDPByDisplayName will find an upstream IDP of type AD (LDAP) by display name", + name: "FindUpstreamIDPByDisplayName will find an upstream IDP of type AD (LDAP) by display name", findIDPByDisplayName: "my-ad-idp1", wrappedLister: testidplister.NewUpstreamIDPListerBuilder(). WithOIDC(myOIDCIDP1). @@ -220,10 +252,25 @@ func TestFederationDomainIdentityProvidersListerFinder(t *testing.T) { WithLDAP(myLDAPIDP1). WithLDAP(myLDAPIDP2). WithActiveDirectory(myADIDP1). + WithGitHub(myGitHubIDP1). BuildDynamicUpstreamIDPProvider(), - federationDomainIssuer: fdIssuerWithOIDCAndLDAPAndADIDPs, + federationDomainIssuer: fdIssuerWithOIDCAndLDAPAndADAndGitHubIDPs, wantLDAPIDPByDisplayName: myADIDP1Resolved, }, + { + name: "FindUpstreamIDPByDisplayName will find an upstream IDP of type GitHub by display name", + findIDPByDisplayName: "my-github-idp1", + wrappedLister: testidplister.NewUpstreamIDPListerBuilder(). + WithOIDC(myOIDCIDP1). + WithOIDC(myOIDCIDP2). + WithLDAP(myLDAPIDP1). + WithLDAP(myLDAPIDP2). + WithActiveDirectory(myADIDP1). + WithGitHub(myGitHubIDP1). + BuildDynamicUpstreamIDPProvider(), + federationDomainIssuer: fdIssuerWithOIDCAndLDAPAndADAndGitHubIDPs, + wantGitHubIDPByDisplayName: myGitHub1Resolved, + }, { name: "FindUpstreamIDPByDisplayName will error if IDP by display name is not found - no such display name", findIDPByDisplayName: "i-cant-find-my-idp", @@ -233,8 +280,9 @@ func TestFederationDomainIdentityProvidersListerFinder(t *testing.T) { WithLDAP(myLDAPIDP1). WithLDAP(myLDAPIDP2). WithActiveDirectory(myADIDP1). + WithGitHub(myGitHubIDP1). BuildDynamicUpstreamIDPProvider(), - federationDomainIssuer: fdIssuerWithOIDCAndLDAPAndADIDPs, + federationDomainIssuer: fdIssuerWithOIDCAndLDAPAndADAndGitHubIDPs, wantError: `identity provider not found: "i-cant-find-my-idp"`, }, { @@ -265,6 +313,9 @@ func TestFederationDomainIdentityProvidersListerFinder(t *testing.T) { if tt.wantLDAPIDPByDisplayName != nil { require.Equal(t, tt.wantLDAPIDPByDisplayName, foundIDP) } + if tt.wantGitHubIDPByDisplayName != nil { + require.Equal(t, tt.wantGitHubIDPByDisplayName, foundIDP) + } }) } @@ -274,6 +325,7 @@ func TestFederationDomainIdentityProvidersListerFinder(t *testing.T) { federationDomainIssuer *FederationDomainIssuer wantDefaultOIDCIDP *resolvedoidc.FederationDomainResolvedOIDCIdentityProvider wantDefaultLDAPIDP *resolvedldap.FederationDomainResolvedLDAPIdentityProvider + wantDefaultGitHubIDP *resolvedgithub.FederationDomainResolvedGitHubIdentityProvider wantError string }{ { @@ -292,6 +344,14 @@ func TestFederationDomainIdentityProvidersListerFinder(t *testing.T) { federationDomainIssuer: fdIssuerWithDefaultLDAPIDP, wantDefaultLDAPIDP: myDefaultLDAPIDPResolved, }, + { + name: "FindDefaultIDP resturns a GitHubIdentityProvider if there is a GitHubIdentityProvider defined as the default IDP", + wrappedLister: testidplister.NewUpstreamIDPListerBuilder(). + WithGitHub(myDefaultGitHubIDP). + BuildDynamicUpstreamIDPProvider(), + federationDomainIssuer: fdIssuerWithDefaultGitHubIDP, + wantDefaultGitHubIDP: myDefaultGitHubIDPResolved, + }, { name: "FindDefaultIDP returns an error if there is no default IDP to return", wrappedLister: testidplister.NewUpstreamIDPListerBuilder(). @@ -340,6 +400,9 @@ func TestFederationDomainIdentityProvidersListerFinder(t *testing.T) { if tt.wantDefaultLDAPIDP != nil { require.Equal(t, tt.wantDefaultLDAPIDP, foundIDP) } + if tt.wantDefaultGitHubIDP != nil { + require.Equal(t, tt.wantDefaultGitHubIDP, foundIDP) + } }) } @@ -357,14 +420,16 @@ func TestFederationDomainIdentityProvidersListerFinder(t *testing.T) { WithLDAP(myLDAPIDP1). WithLDAP(myLDAPIDP2). WithActiveDirectory(myADIDP1). + WithGitHub(myGitHubIDP1). BuildDynamicUpstreamIDPProvider(), - federationDomainIssuer: fdIssuerWithOIDCAndLDAPAndADIDPs, + federationDomainIssuer: fdIssuerWithOIDCAndLDAPAndADAndGitHubIDPs, wantIDPs: []resolvedprovider.FederationDomainResolvedIdentityProvider{ myOIDCIDP1Resolved, myOIDCIDP2Resolved, myLDAPIDP1Resolved, myLDAPIDP2Resolved, myADIDP1Resolved, + myGitHub1Resolved, }, }, { @@ -378,6 +443,7 @@ func TestFederationDomainIdentityProvidersListerFinder(t *testing.T) { Build()). WithLDAP(myLDAPIDP1). WithActiveDirectory(myADIDP1). + WithGitHub(myGitHubIDP1). BuildDynamicUpstreamIDPProvider(), federationDomainIssuer: fdIssuerWithLotsOfIDPs, wantIDPs: []resolvedprovider.FederationDomainResolvedIdentityProvider{ @@ -385,13 +451,14 @@ func TestFederationDomainIdentityProvidersListerFinder(t *testing.T) { myOIDCIDP2Resolved, myLDAPIDP1Resolved, myADIDP1Resolved, + myGitHub1Resolved, }, }, { name: "GetIdentityProviders will return empty list if no IDPs are found", wrappedLister: testidplister.NewUpstreamIDPListerBuilder(). BuildDynamicUpstreamIDPProvider(), - federationDomainIssuer: fdIssuerWithOIDCAndLDAPAndADIDPs, + federationDomainIssuer: fdIssuerWithOIDCAndLDAPAndADAndGitHubIDPs, wantIDPs: []resolvedprovider.FederationDomainResolvedIdentityProvider{}, }, } @@ -417,7 +484,7 @@ func TestFederationDomainIdentityProvidersListerFinder(t *testing.T) { name: "IDPCount when there are none to be found", wrappedLister: testidplister.NewUpstreamIDPListerBuilder(). BuildDynamicUpstreamIDPProvider(), - federationDomainIssuer: fdIssuerWithOIDCAndLDAPAndADIDPs, + federationDomainIssuer: fdIssuerWithOIDCAndLDAPAndADAndGitHubIDPs, wantCount: 0, }, { @@ -440,9 +507,14 @@ func TestFederationDomainIdentityProvidersListerFinder(t *testing.T) { WithName("my-ad-idp-that-isnt-in-fd-issuer"). WithResourceUID("my-ad-idp-that-isnt-in-fd-issuer"). Build()). + WithGitHub(myGitHubIDP1). + WithGitHub(oidctestutil.NewTestUpstreamGitHubIdentityProviderBuilder(). + WithName("my-github-idp-that-isnt-in-fd-issuer"). + WithResourceUID("my-github-idp-that-isnt-in-fd-issuer"). + Build()). BuildDynamicUpstreamIDPProvider(), - federationDomainIssuer: fdIssuerWithOIDCAndLDAPAndADIDPs, - wantCount: 5, + federationDomainIssuer: fdIssuerWithOIDCAndLDAPAndADAndGitHubIDPs, + wantCount: 6, }, } @@ -478,6 +550,14 @@ func TestFederationDomainIdentityProvidersListerFinder(t *testing.T) { federationDomainIssuer: fdIssuerWithDefaultLDAPIDP, wantHasDefaultIDP: true, }, + { + name: "HasDefaultIDP when there is a GitHub provider set as default", + wrappedLister: testidplister.NewUpstreamIDPListerBuilder(). + WithGitHub(myDefaultGitHubIDP). + BuildDynamicUpstreamIDPProvider(), + federationDomainIssuer: fdIssuerWithDefaultGitHubIDP, + wantHasDefaultIDP: true, + }, { name: "HasDefaultIDP when there is one set even if it cannot be found", wrappedLister: testidplister.NewUpstreamIDPListerBuilder(). @@ -493,7 +573,7 @@ func TestFederationDomainIdentityProvidersListerFinder(t *testing.T) { name: "HasDefaultIDP when there is none set", wrappedLister: testidplister.NewUpstreamIDPListerBuilder(). BuildDynamicUpstreamIDPProvider(), - federationDomainIssuer: fdIssuerWithOIDCAndLDAPAndADIDPs, + federationDomainIssuer: fdIssuerWithOIDCAndLDAPAndADAndGitHubIDPs, wantHasDefaultIDP: false, }, } diff --git a/internal/federationdomain/idplister/upstream_idp_lister.go b/internal/federationdomain/idplister/upstream_idp_lister.go index 38b5e27eb..0084b472c 100644 --- a/internal/federationdomain/idplister/upstream_idp_lister.go +++ b/internal/federationdomain/idplister/upstream_idp_lister.go @@ -1,4 +1,4 @@ -// Copyright 2020-2023 the Pinniped contributors. All Rights Reserved. +// Copyright 2020-2024 the Pinniped contributors. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 package idplister @@ -19,8 +19,13 @@ type UpstreamActiveDirectoryIdentityProviderLister interface { GetActiveDirectoryIdentityProviders() []upstreamprovider.UpstreamLDAPIdentityProviderI } +type UpstreamGitHubIdentityProviderLister interface { + GetGitHubIdentityProviders() []upstreamprovider.UpstreamGithubIdentityProviderI +} + type UpstreamIdentityProvidersLister interface { UpstreamOIDCIdentityProvidersLister UpstreamLDAPIdentityProvidersLister UpstreamActiveDirectoryIdentityProviderLister + UpstreamGitHubIdentityProviderLister } diff --git a/internal/federationdomain/resolvedprovider/resolvedgithub/resolved_github_provider.go b/internal/federationdomain/resolvedprovider/resolvedgithub/resolved_github_provider.go new file mode 100644 index 000000000..eb846b3e4 --- /dev/null +++ b/internal/federationdomain/resolvedprovider/resolvedgithub/resolved_github_provider.go @@ -0,0 +1,179 @@ +// Copyright 2020-2024 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package resolvedgithub + +import ( + "context" + "errors" + "fmt" + "net/http" + + "github.com/ory/fosite" + "golang.org/x/oauth2" + + "go.pinniped.dev/generated/latest/apis/supervisor/idpdiscovery/v1alpha1" + "go.pinniped.dev/internal/federationdomain/resolvedprovider" + "go.pinniped.dev/internal/federationdomain/upstreamprovider" + "go.pinniped.dev/internal/httputil/httperr" + "go.pinniped.dev/internal/idtransform" + "go.pinniped.dev/internal/psession" + "go.pinniped.dev/pkg/oidcclient/nonce" + "go.pinniped.dev/pkg/oidcclient/pkce" +) + +// FederationDomainResolvedGitHubIdentityProvider represents a FederationDomainIdentityProvider which has +// been resolved dynamically based on the currently loaded IDP CRs to include the provider.UpstreamGitHubIdentityProviderI +// and other metadata about the provider. +type FederationDomainResolvedGitHubIdentityProvider struct { + DisplayName string + Provider upstreamprovider.UpstreamGithubIdentityProviderI + SessionProviderType psession.ProviderType + Transforms *idtransform.TransformationPipeline +} + +var _ resolvedprovider.FederationDomainResolvedIdentityProvider = (*FederationDomainResolvedGitHubIdentityProvider)(nil) + +func (p *FederationDomainResolvedGitHubIdentityProvider) GetDisplayName() string { + return p.DisplayName +} + +func (p *FederationDomainResolvedGitHubIdentityProvider) GetProvider() upstreamprovider.UpstreamIdentityProviderI { + return p.Provider +} + +func (p *FederationDomainResolvedGitHubIdentityProvider) GetSessionProviderType() psession.ProviderType { + return p.SessionProviderType +} + +func (p *FederationDomainResolvedGitHubIdentityProvider) GetIDPDiscoveryType() v1alpha1.IDPType { + return v1alpha1.IDPTypeGitHub +} + +func (p *FederationDomainResolvedGitHubIdentityProvider) GetIDPDiscoveryFlows() []v1alpha1.IDPFlow { + return []v1alpha1.IDPFlow{v1alpha1.IDPFlowBrowserAuthcode} +} + +func (p *FederationDomainResolvedGitHubIdentityProvider) GetTransforms() *idtransform.TransformationPipeline { + return p.Transforms +} + +func (p *FederationDomainResolvedGitHubIdentityProvider) CloneIDPSpecificSessionDataFromSession(session *psession.CustomSessionData) interface{} { + if session.GitHub == nil { + return nil + } + return session.GitHub.Clone() +} + +func (p *FederationDomainResolvedGitHubIdentityProvider) ApplyIDPSpecificSessionDataToSession(session *psession.CustomSessionData, idpSpecificSessionData interface{}) { + session.GitHub = idpSpecificSessionData.(*psession.GitHubSessionData) +} + +func (p *FederationDomainResolvedGitHubIdentityProvider) UpstreamAuthorizeRedirectURL( + state *resolvedprovider.UpstreamAuthorizeRequestState, + downstreamIssuerURL string, +) (string, error) { + upstreamOAuthConfig := oauth2.Config{ + ClientID: p.Provider.GetClientID(), + Endpoint: oauth2.Endpoint{ + AuthURL: p.Provider.GetAuthorizationURL(), + }, + RedirectURL: fmt.Sprintf("%s/callback", downstreamIssuerURL), + Scopes: p.Provider.GetScopes(), + } + redirectURL := upstreamOAuthConfig.AuthCodeURL(state.EncodedStateParam) + return redirectURL, nil +} + +func (p *FederationDomainResolvedGitHubIdentityProvider) Login( + _ context.Context, + _ string, + _ string, +) (*resolvedprovider.Identity, *resolvedprovider.IdentityLoginExtras, error) { + return nil, nil, errors.New("function Login not yet implemented for GitHub IDP") +} + +func (p *FederationDomainResolvedGitHubIdentityProvider) LoginFromCallback( + ctx context.Context, + authCode string, + _ pkce.Code, // GitHub does not support PKCE, see https://docs.github.com/en/apps/oauth-apps/building-oauth-apps/authorizing-oauth-apps + _ nonce.Nonce, // GitHub does not support OIDC, therefore there is no ID token that could contain the "nonce". + redirectURI string, +) (*resolvedprovider.Identity, *resolvedprovider.IdentityLoginExtras, error) { + accessToken, err := p.Provider.ExchangeAuthcode(ctx, authCode, redirectURI) + if err != nil { + return nil, nil, httperr.Wrap(http.StatusBadGateway, + "failed to exchange authcode using GitHub API", + err, + ) + } + + user, err := p.Provider.GetUser(ctx, accessToken, p.GetDisplayName()) + + if errors.As(err, &upstreamprovider.GitHubLoginDeniedError{}) { + // We specifically want errors of type GitHubLoginDeniedError to have a user-displayed message. + // Don't wrap the error since we include it in the sprintf here. + return nil, nil, httperr.Newf(http.StatusForbidden, + "login denied due to configuration on GitHubIdentityProvider with display name %q: %s", + p.GetDisplayName(), err) + } else if err != nil { + return nil, nil, httperr.Wrap(http.StatusUnprocessableEntity, + "failed to get user info from GitHub API", + err, + ) + } + + return &resolvedprovider.Identity{ + UpstreamUsername: user.Username, + UpstreamGroups: user.Groups, + DownstreamSubject: user.DownstreamSubject, + IDPSpecificSessionData: &psession.GitHubSessionData{ + UpstreamAccessToken: accessToken, + }, + }, + &resolvedprovider.IdentityLoginExtras{ + DownstreamAdditionalClaims: nil, // not using this for GitHub + Warnings: nil, // not using this for GitHub + }, + nil // no error +} + +func (p *FederationDomainResolvedGitHubIdentityProvider) UpstreamRefresh( + ctx context.Context, + identity *resolvedprovider.Identity, +) (*resolvedprovider.RefreshedIdentity, error) { + githubSessionData, ok := identity.IDPSpecificSessionData.(*psession.GitHubSessionData) + if !ok { + // This should not really happen. + return nil, p.refreshErr(errors.New("wrong data type found for IDPSpecificSessionData")) + } + if len(githubSessionData.UpstreamAccessToken) == 0 { + // This should not really happen. + return nil, p.refreshErr(errors.New("session is missing GitHub access token")) + } + + // Get the user's GitHub identity and groups again using the cached access token. + refreshedUserInfo, err := p.Provider.GetUser(ctx, githubSessionData.UpstreamAccessToken, p.GetDisplayName()) + if err != nil { + return nil, p.refreshErr(err) + } + + if refreshedUserInfo.DownstreamSubject != identity.DownstreamSubject { + // The user's upstream identity changed since the initial login in a surprising way. + return nil, p.refreshErr(fmt.Errorf("user's calculated downstream subject at initial login was %q but now is %q", + identity.DownstreamSubject, refreshedUserInfo.DownstreamSubject)) + } + + return &resolvedprovider.RefreshedIdentity{ + UpstreamUsername: refreshedUserInfo.Username, + UpstreamGroups: refreshedUserInfo.Groups, + IDPSpecificSessionData: nil, // nil means that no update to the GitHub-specific portion of the session data is required + }, nil +} + +func (p *FederationDomainResolvedGitHubIdentityProvider) refreshErr(err error) *fosite.RFC6749Error { + return resolvedprovider.ErrUpstreamRefreshError(). + WithHint("Upstream refresh failed."). + WithTrace(err). + WithDebugf("provider name: %q, provider type: %q", p.Provider.GetResourceName(), p.GetSessionProviderType()) +} diff --git a/internal/federationdomain/resolvedprovider/resolvedgithub/resolved_github_provider_test.go b/internal/federationdomain/resolvedprovider/resolvedgithub/resolved_github_provider_test.go new file mode 100644 index 000000000..409a22cf1 --- /dev/null +++ b/internal/federationdomain/resolvedprovider/resolvedgithub/resolved_github_provider_test.go @@ -0,0 +1,452 @@ +// Copyright 2020-2024 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package resolvedgithub + +import ( + "context" + "errors" + "net/http" + "net/http/httptest" + "testing" + + "github.com/ory/fosite" + "github.com/stretchr/testify/require" + "golang.org/x/oauth2" + + idpv1alpha1 "go.pinniped.dev/generated/latest/apis/supervisor/idp/v1alpha1" + idpdiscoveryv1alpha1 "go.pinniped.dev/generated/latest/apis/supervisor/idpdiscovery/v1alpha1" + "go.pinniped.dev/internal/federationdomain/resolvedprovider" + "go.pinniped.dev/internal/federationdomain/upstreamprovider" + "go.pinniped.dev/internal/httputil/httperr" + "go.pinniped.dev/internal/psession" + "go.pinniped.dev/internal/setutil" + "go.pinniped.dev/internal/testutil/oidctestutil" + "go.pinniped.dev/internal/testutil/transformtestutil" + "go.pinniped.dev/internal/upstreamgithub" +) + +func TestFederationDomainResolvedGitHubIdentityProvider(t *testing.T) { + transforms := transformtestutil.NewRejectAllAuthPipeline(t) + + provider := upstreamgithub.New(upstreamgithub.ProviderConfig{ + Name: "fake-provider-config", + ResourceUID: "fake-resource-uid", + APIBaseURL: "https://fake-api-host.com", + UsernameAttribute: idpv1alpha1.GitHubUsernameID, + GroupNameAttribute: idpv1alpha1.GitHubUseTeamSlugForGroupName, + AllowedOrganizations: setutil.NewCaseInsensitiveSet("org1", "org2"), + HttpClient: nil, // not needed yet for this test + OAuth2Config: &oauth2.Config{ + ClientID: "fake-client-id", + ClientSecret: "fake-client-secret", + Scopes: []string{"read:user", "read:org"}, + Endpoint: oauth2.Endpoint{ + AuthURL: "https://fake-authorization-url", + DeviceAuthURL: "", + TokenURL: "https://fake-token-url", + AuthStyle: oauth2.AuthStyleInParams, + }, + }, + }) + + subject := FederationDomainResolvedGitHubIdentityProvider{ + DisplayName: "fake-display-name", + Provider: provider, + SessionProviderType: psession.ProviderTypeGitHub, + Transforms: transforms, + } + + require.Equal(t, "fake-display-name", subject.GetDisplayName()) + require.Equal(t, provider, subject.GetProvider()) + require.Equal(t, psession.ProviderTypeGitHub, subject.GetSessionProviderType()) + require.Equal(t, idpdiscoveryv1alpha1.IDPTypeGitHub, subject.GetIDPDiscoveryType()) + require.Equal(t, []idpdiscoveryv1alpha1.IDPFlow{idpdiscoveryv1alpha1.IDPFlowBrowserAuthcode}, subject.GetIDPDiscoveryFlows()) + require.Equal(t, transforms, subject.GetTransforms()) + + originalCustomSession := &psession.CustomSessionData{ + Username: "fake-username", + UpstreamUsername: "fake-upstream-username", + GitHub: &psession.GitHubSessionData{UpstreamAccessToken: "fake-upstream-access-token"}, + } + clonedCustomSession := subject.CloneIDPSpecificSessionDataFromSession(originalCustomSession) + require.Equal(t, + &psession.GitHubSessionData{UpstreamAccessToken: "fake-upstream-access-token"}, + clonedCustomSession, + ) + require.NotSame(t, originalCustomSession, clonedCustomSession) + + customSessionToBeMutated := &psession.CustomSessionData{ + Username: "fake-username2", + UpstreamUsername: "fake-upstream-username2", + } + subject.ApplyIDPSpecificSessionDataToSession(customSessionToBeMutated, &psession.GitHubSessionData{UpstreamAccessToken: "OTHER-upstream-access-token"}) + require.Equal(t, &psession.CustomSessionData{ + Username: "fake-username2", + UpstreamUsername: "fake-upstream-username2", + GitHub: &psession.GitHubSessionData{UpstreamAccessToken: "OTHER-upstream-access-token"}, + }, customSessionToBeMutated) + + redirectURL, err := subject.UpstreamAuthorizeRedirectURL( + &resolvedprovider.UpstreamAuthorizeRequestState{ + EncodedStateParam: "encodedStateParam12345", + PKCE: "pkce6789", + Nonce: "nonce1289", + }, + "https://localhost/fake/path", + ) + require.NoError(t, err) + // Note that GitHub does not require (or document) the standard response_type=code param, but in manual testing + // of GitHub authorize endpoint, it seems to ignore the param. The oauth2 package wants to add the param, so + // we will let it. + require.Equal(t, + "https://fake-authorization-url?"+ + "client_id=fake-client-id&"+ + "redirect_uri=https%3A%2F%2Flocalhost%2Ffake%2Fpath%2Fcallback&"+ + "response_type=code&"+ + "scope=read%3Auser+read%3Aorg&"+ + "state=encodedStateParam12345", + redirectURL, + ) +} + +func TestLoginFromCallback(t *testing.T) { + uniqueCtx := context.WithValue(context.Background(), "some-unique-key", "some-value") //nolint:staticcheck // okay to use string key for test + + tests := []struct { + name string + provider *oidctestutil.TestUpstreamGitHubIdentityProvider + idpDisplayName string + authcode string + redirectURI string + + wantExchangeAuthcodeCall bool + wantExchangeAuthcodeArgs *oidctestutil.ExchangeAuthcodeArgs + wantGetUserCall bool + wantGetUserArgs *oidctestutil.GetUserArgs + wantIdentity *resolvedprovider.Identity + wantExtras *resolvedprovider.IdentityLoginExtras + wantErrMsg string + wantErrResponseMsg string + wantErrStatusCode int + }{ + { + name: "happy path", + provider: oidctestutil.NewTestUpstreamGitHubIdentityProviderBuilder(). + WithAccessToken("fake-access-token"). + WithUser(&upstreamprovider.GitHubUser{ + Username: "fake-username", + Groups: []string{"fake-group1", "fake-group2"}, + DownstreamSubject: "https://fake-downstream-subject", + }). + Build(), + idpDisplayName: "fake-display-name", + authcode: "fake-authcode", + redirectURI: "https://fake-redirect-uri", + wantExchangeAuthcodeCall: true, + wantExchangeAuthcodeArgs: &oidctestutil.ExchangeAuthcodeArgs{ + Ctx: uniqueCtx, + Authcode: "fake-authcode", + RedirectURI: "https://fake-redirect-uri", + }, + wantGetUserCall: true, + wantGetUserArgs: &oidctestutil.GetUserArgs{ + Ctx: uniqueCtx, + AccessToken: "fake-access-token", + IDPDisplayName: "fake-display-name", + }, + wantIdentity: &resolvedprovider.Identity{ + UpstreamUsername: "fake-username", + UpstreamGroups: []string{"fake-group1", "fake-group2"}, + DownstreamSubject: "https://fake-downstream-subject", + IDPSpecificSessionData: &psession.GitHubSessionData{ + UpstreamAccessToken: "fake-access-token", + }, + }, + wantExtras: &resolvedprovider.IdentityLoginExtras{}, + }, + { + name: "error while exchanging authcode", + provider: oidctestutil.NewTestUpstreamGitHubIdentityProviderBuilder(). + WithAuthcodeExchangeError(errors.New("fake authcode exchange error")). + Build(), + idpDisplayName: "fake-display-name", + authcode: "fake-authcode", + redirectURI: "https://fake-redirect-uri", + wantExchangeAuthcodeCall: true, + wantExchangeAuthcodeArgs: &oidctestutil.ExchangeAuthcodeArgs{ + Ctx: uniqueCtx, + Authcode: "fake-authcode", + RedirectURI: "https://fake-redirect-uri", + }, + wantGetUserCall: false, + wantIdentity: nil, + wantExtras: nil, + wantErrMsg: "failed to exchange authcode using GitHub API: fake authcode exchange error", + wantErrResponseMsg: "Bad Gateway: failed to exchange authcode using GitHub API", + wantErrStatusCode: http.StatusBadGateway, + }, + { + name: "generic error while getting user info", + provider: oidctestutil.NewTestUpstreamGitHubIdentityProviderBuilder(). + WithAccessToken("fake-access-token"). + WithGetUserError(errors.New("fake user info error")). + Build(), + idpDisplayName: "fake-display-name", + authcode: "fake-authcode", + redirectURI: "https://fake-redirect-uri", + wantExchangeAuthcodeCall: true, + wantExchangeAuthcodeArgs: &oidctestutil.ExchangeAuthcodeArgs{ + Ctx: uniqueCtx, + Authcode: "fake-authcode", + RedirectURI: "https://fake-redirect-uri", + }, + wantGetUserCall: true, + wantGetUserArgs: &oidctestutil.GetUserArgs{ + Ctx: uniqueCtx, + AccessToken: "fake-access-token", + IDPDisplayName: "fake-display-name", + }, + wantIdentity: nil, + wantExtras: nil, + wantErrMsg: "failed to get user info from GitHub API: fake user info error", + wantErrResponseMsg: "Unprocessable Entity: failed to get user info from GitHub API", + wantErrStatusCode: http.StatusUnprocessableEntity, + }, + { + name: "loginDenied error while getting user info", + provider: oidctestutil.NewTestUpstreamGitHubIdentityProviderBuilder(). + WithAccessToken("fake-access-token"). + WithGetUserError(upstreamprovider.NewGitHubLoginDeniedError("some login denied error")). + Build(), + idpDisplayName: "fake-display-name", + authcode: "fake-authcode", + redirectURI: "https://fake-redirect-uri", + wantExchangeAuthcodeCall: true, + wantExchangeAuthcodeArgs: &oidctestutil.ExchangeAuthcodeArgs{ + Ctx: uniqueCtx, + Authcode: "fake-authcode", + RedirectURI: "https://fake-redirect-uri", + }, + wantGetUserCall: true, + wantGetUserArgs: &oidctestutil.GetUserArgs{ + Ctx: uniqueCtx, + AccessToken: "fake-access-token", + IDPDisplayName: "fake-display-name", + }, + wantIdentity: nil, + wantExtras: nil, + wantErrMsg: `login denied due to configuration on GitHubIdentityProvider with display name "fake-display-name": some login denied error`, + wantErrResponseMsg: `Forbidden: login denied due to configuration on GitHubIdentityProvider with display name "fake-display-name": some login denied error`, + wantErrStatusCode: http.StatusForbidden, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + subject := FederationDomainResolvedGitHubIdentityProvider{ + DisplayName: test.idpDisplayName, + Provider: test.provider, + SessionProviderType: psession.ProviderTypeGitHub, + Transforms: transformtestutil.NewRejectAllAuthPipeline(t), + } + + identity, loginExtras, err := subject.LoginFromCallback(uniqueCtx, + test.authcode, + "pkce-will-be-ignored", + "nonce-will-be-ignored", + test.redirectURI, + ) + + if test.wantExchangeAuthcodeCall { + require.Equal(t, 1, test.provider.ExchangeAuthcodeCallCount()) + require.Equal(t, test.wantExchangeAuthcodeArgs, test.provider.ExchangeAuthcodeArgs(0)) + } else { + require.Zero(t, test.provider.ExchangeAuthcodeCallCount()) + } + + if test.wantGetUserCall { + require.Equal(t, 1, test.provider.GetUserCallCount()) + require.Equal(t, test.wantGetUserArgs, test.provider.GetUserArgs(0)) + } else { + require.Zero(t, test.provider.GetUserCallCount()) + } + + if test.wantErrResponseMsg == "" { + require.NoError(t, err) + } else { + require.Implements(t, (*httperr.Responder)(nil), err) + errAsResponder := err.(httperr.Responder) + rec := httptest.NewRecorder() + errAsResponder.Respond(rec) + require.Equal(t, test.wantErrStatusCode, rec.Code) + require.Equal(t, test.wantErrResponseMsg+"\n", rec.Body.String()) + + require.EqualError(t, errAsResponder, test.wantErrMsg) + } + require.Equal(t, test.wantExtras, loginExtras) + require.Equal(t, test.wantIdentity, identity) + }) + } +} + +func TestUpstreamRefresh(t *testing.T) { + uniqueCtx := context.WithValue(context.Background(), "some-unique-key", "some-value") //nolint:staticcheck // okay to use string key for test + + tests := []struct { + name string + provider *oidctestutil.TestUpstreamGitHubIdentityProvider + idpDisplayName string + identity *resolvedprovider.Identity + + wantGetUserCall bool + wantGetUserArgs *oidctestutil.GetUserArgs + wantRefreshedIdentity *resolvedprovider.RefreshedIdentity + wantWrappedErr string + }{ + { + name: "happy path", + provider: oidctestutil.NewTestUpstreamGitHubIdentityProviderBuilder(). + WithUser(&upstreamprovider.GitHubUser{ + Username: "refreshed-username", + Groups: []string{"refreshed-group1", "refreshed-group2"}, + DownstreamSubject: "https://fake-downstream-subject", + }). + Build(), + identity: &resolvedprovider.Identity{ + UpstreamUsername: "initial-username", + UpstreamGroups: []string{"initial-group1", "initial-group2"}, + DownstreamSubject: "https://fake-downstream-subject", + IDPSpecificSessionData: &psession.GitHubSessionData{UpstreamAccessToken: "fake-access-token"}, + }, + idpDisplayName: "fake-display-name", + wantGetUserCall: true, + wantGetUserArgs: &oidctestutil.GetUserArgs{ + Ctx: uniqueCtx, + AccessToken: "fake-access-token", + IDPDisplayName: "fake-display-name", + }, + wantRefreshedIdentity: &resolvedprovider.RefreshedIdentity{ + UpstreamUsername: "refreshed-username", + UpstreamGroups: []string{"refreshed-group1", "refreshed-group2"}, + IDPSpecificSessionData: nil, + }, + }, + { + name: "error while getting user info", + provider: oidctestutil.NewTestUpstreamGitHubIdentityProviderBuilder(). + WithName("fake-provider-name"). + WithGetUserError(errors.New("fake github GetUser error message")). + Build(), + identity: &resolvedprovider.Identity{ + UpstreamUsername: "initial-username", + UpstreamGroups: []string{"initial-group1", "initial-group2"}, + DownstreamSubject: "https://fake-downstream-subject", + IDPSpecificSessionData: &psession.GitHubSessionData{UpstreamAccessToken: "fake-access-token"}, + }, + idpDisplayName: "fake-display-name", + wantGetUserCall: true, + wantGetUserArgs: &oidctestutil.GetUserArgs{ + Ctx: uniqueCtx, + AccessToken: "fake-access-token", + IDPDisplayName: "fake-display-name", + }, + wantRefreshedIdentity: nil, + wantWrappedErr: "fake github GetUser error message", + }, + { + name: "wrong session data type, which should not really happen", + provider: oidctestutil.NewTestUpstreamGitHubIdentityProviderBuilder(). + WithName("fake-provider-name"). + Build(), + identity: &resolvedprovider.Identity{ + UpstreamUsername: "initial-username", + UpstreamGroups: []string{"initial-group1", "initial-group2"}, + DownstreamSubject: "https://fake-downstream-subject", + IDPSpecificSessionData: &psession.LDAPSessionData{}, // wrong type + }, + idpDisplayName: "fake-display-name", + wantGetUserCall: false, + wantRefreshedIdentity: nil, + wantWrappedErr: "wrong data type found for IDPSpecificSessionData", + }, + { + name: "session is missing github access token, which should not really happen", + provider: oidctestutil.NewTestUpstreamGitHubIdentityProviderBuilder(). + WithName("fake-provider-name"). + Build(), + identity: &resolvedprovider.Identity{ + UpstreamUsername: "initial-username", + UpstreamGroups: []string{"initial-group1", "initial-group2"}, + DownstreamSubject: "https://fake-downstream-subject", + IDPSpecificSessionData: &psession.GitHubSessionData{UpstreamAccessToken: ""}, // missing token + }, + idpDisplayName: "fake-display-name", + wantGetUserCall: false, + wantRefreshedIdentity: nil, + wantWrappedErr: "session is missing GitHub access token", + }, + { + name: "users downstream subject changes based on an unexpected change in the upstream identity", + provider: oidctestutil.NewTestUpstreamGitHubIdentityProviderBuilder(). + WithName("fake-provider-name"). + WithUser(&upstreamprovider.GitHubUser{ + Username: "refreshed-username", + Groups: []string{"refreshed-group1", "refreshed-group2"}, + DownstreamSubject: "https://unexpected-different-downstream-subject", // unexpected change in calculated subject during refresh + }). + Build(), + identity: &resolvedprovider.Identity{ + UpstreamUsername: "initial-username", + UpstreamGroups: []string{"initial-group1", "initial-group2"}, + DownstreamSubject: "https://fake-downstream-subject", + IDPSpecificSessionData: &psession.GitHubSessionData{UpstreamAccessToken: "fake-access-token"}, + }, + idpDisplayName: "fake-display-name", + wantGetUserCall: true, + wantGetUserArgs: &oidctestutil.GetUserArgs{ + Ctx: uniqueCtx, + AccessToken: "fake-access-token", + IDPDisplayName: "fake-display-name", + }, + wantRefreshedIdentity: nil, + wantWrappedErr: `user's calculated downstream subject at initial login was "https://fake-downstream-subject" ` + + `but now is "https://unexpected-different-downstream-subject"`, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + subject := FederationDomainResolvedGitHubIdentityProvider{ + DisplayName: test.idpDisplayName, + Provider: test.provider, + SessionProviderType: psession.ProviderTypeGitHub, + Transforms: transformtestutil.NewRejectAllAuthPipeline(t), + } + + refreshedIdentity, err := subject.UpstreamRefresh(uniqueCtx, test.identity) + + if test.wantGetUserCall { + require.Equal(t, 1, test.provider.GetUserCallCount()) + require.Equal(t, test.wantGetUserArgs, test.provider.GetUserArgs(0)) + } else { + require.Zero(t, test.provider.GetUserCallCount()) + } + + if test.wantWrappedErr == "" { + require.NoError(t, err) + } else { + require.NotNil(t, err, "expected to get an error but did not get one") + errAsFositeErr, ok := err.(*fosite.RFC6749Error) + require.True(t, ok) + require.EqualError(t, errAsFositeErr.Unwrap(), test.wantWrappedErr) + require.Equal(t, "error", errAsFositeErr.ErrorField) + require.Equal(t, "Error during upstream refresh.", errAsFositeErr.DescriptionField) + require.Equal(t, http.StatusUnauthorized, errAsFositeErr.CodeField) + require.Equal(t, `provider name: "fake-provider-name", provider type: "github"`, errAsFositeErr.DebugField) + } + + require.Equal(t, test.wantRefreshedIdentity, refreshedIdentity) + }) + } +} diff --git a/internal/federationdomain/resolvedprovider/resolvedldap/resolved_ldap_provider.go b/internal/federationdomain/resolvedprovider/resolvedldap/resolved_ldap_provider.go index aab159eb1..5a989f31a 100644 --- a/internal/federationdomain/resolvedprovider/resolvedldap/resolved_ldap_provider.go +++ b/internal/federationdomain/resolvedprovider/resolvedldap/resolved_ldap_provider.go @@ -76,7 +76,7 @@ func (p *FederationDomainResolvedLDAPIdentityProvider) CloneIDPSpecificSessionDa return nil } return session.ActiveDirectory.Clone() - case psession.ProviderTypeOIDC: // this is just here to avoid a lint error about not handling all cases + case psession.ProviderTypeOIDC, psession.ProviderTypeGitHub: // this is just here to avoid a lint error about not handling all cases fallthrough default: return nil @@ -128,7 +128,7 @@ func (p *FederationDomainResolvedLDAPIdentityProvider) Login( ) (*resolvedprovider.Identity, *resolvedprovider.IdentityLoginExtras, error) { authenticateResponse, authenticated, err := p.Provider.AuthenticateUser(ctx, submittedUsername, submittedPassword) if err != nil { - plog.WarningErr("unexpected error during upstream LDAP authentication", err, "upstreamName", p.Provider.GetName()) + plog.WarningErr("unexpected error during upstream LDAP authentication", err, "upstreamName", p.Provider.GetResourceName()) return nil, nil, ErrUnexpectedUpstreamLDAPError.WithWrap(err) } if !authenticated { @@ -151,7 +151,7 @@ func (p *FederationDomainResolvedLDAPIdentityProvider) Login( UserDN: authenticateResponse.DN, ExtraRefreshAttributes: authenticateResponse.ExtraRefreshAttributes, } - case psession.ProviderTypeOIDC: // this is just here to avoid a lint error about not handling all cases + case psession.ProviderTypeOIDC, psession.ProviderTypeGitHub: // this is just here to avoid a lint error about not handling all cases fallthrough default: return nil, nil, ErrUnexpectedUpstreamLDAPError.WithWrap(fmt.Errorf("unexpected provider type %q", p.GetSessionProviderType())) @@ -205,13 +205,13 @@ func (p *FederationDomainResolvedLDAPIdentityProvider) UpstreamRefresh( } dn = sessionData.UserDN additionalAttributes = sessionData.ExtraRefreshAttributes - case psession.ProviderTypeOIDC: // this is just here to avoid a lint error about not handling all cases + case psession.ProviderTypeOIDC, psession.ProviderTypeGitHub: // this is just here to avoid a lint error about not handling all cases fallthrough default: // This shouldn't really happen. return nil, resolvedprovider.ErrUpstreamRefreshError().WithHintf( "Unexpected provider type during refresh %q", p.GetSessionProviderType()).WithTrace(err). - WithDebugf("provider name: %q, provider type: %q", p.Provider.GetName(), p.GetSessionProviderType()) + WithDebugf("provider name: %q, provider type: %q", p.Provider.GetResourceName(), p.GetSessionProviderType()) } if dn == "" { @@ -219,9 +219,11 @@ func (p *FederationDomainResolvedLDAPIdentityProvider) UpstreamRefresh( } plog.Debug("attempting upstream refresh request", - "providerName", p.Provider.GetName(), "providerType", p.GetSessionProviderType(), "providerUID", p.Provider.GetResourceUID()) + "identityProviderResourceName", p.Provider.GetResourceName(), + "identityProviderType", p.GetSessionProviderType(), + "identityProviderUID", p.Provider.GetResourceUID()) - refreshedUntransformedGroups, err := p.Provider.PerformRefresh(ctx, upstreamprovider.RefreshAttributes{ + refreshedUntransformedGroups, err := p.Provider.PerformRefresh(ctx, upstreamprovider.LDAPRefreshAttributes{ Username: identity.UpstreamUsername, Subject: identity.DownstreamSubject, DN: dn, @@ -231,7 +233,7 @@ func (p *FederationDomainResolvedLDAPIdentityProvider) UpstreamRefresh( if err != nil { return nil, resolvedprovider.ErrUpstreamRefreshError().WithHint( "Upstream refresh failed.").WithTrace(err). - WithDebugf("provider name: %q, provider type: %q", p.Provider.GetName(), p.GetSessionProviderType()) + WithDebugf("provider name: %q, provider type: %q", p.Provider.GetResourceName(), p.GetSessionProviderType()) } return &resolvedprovider.RefreshedIdentity{ diff --git a/internal/federationdomain/resolvedprovider/resolvedoidc/resolved_oidc_provider.go b/internal/federationdomain/resolvedprovider/resolvedoidc/resolved_oidc_provider.go index 21d155a4b..a2e3644dd 100644 --- a/internal/federationdomain/resolvedprovider/resolvedoidc/resolved_oidc_provider.go +++ b/internal/federationdomain/resolvedprovider/resolvedoidc/resolved_oidc_provider.go @@ -191,8 +191,7 @@ func (p *FederationDomainResolvedOIDCIdentityProvider) LoginFromCallback( redirectURI, ) if err != nil { - plog.WarningErr("error exchanging and validating upstream tokens", err, "upstreamName", p.Provider.GetName()) - return nil, nil, httperr.New(http.StatusBadGateway, "error exchanging and validating upstream tokens") + return nil, nil, httperr.Wrap(http.StatusBadGateway, "error exchanging and validating upstream tokens", err) } subject, upstreamUsername, upstreamGroups, err := getIdentityFromUpstreamIDToken( @@ -241,7 +240,9 @@ func (p *FederationDomainResolvedOIDCIdentityProvider) UpstreamRefresh( } plog.Debug("attempting upstream refresh request", - "providerName", p.Provider.GetName(), "providerType", p.GetSessionProviderType(), "providerUID", p.Provider.GetResourceUID()) + "identityProviderResourceName", p.Provider.GetResourceName(), + "identityProviderType", p.GetSessionProviderType(), + "identityProviderUID", p.Provider.GetResourceUID()) var tokens *oauth2.Token if refreshTokenStored { @@ -249,7 +250,7 @@ func (p *FederationDomainResolvedOIDCIdentityProvider) UpstreamRefresh( if err != nil { return nil, resolvedprovider.ErrUpstreamRefreshError().WithHint( "Upstream refresh failed.", - ).WithTrace(err).WithDebugf("provider name: %q, provider type: %q", p.Provider.GetName(), p.GetSessionProviderType()) + ).WithTrace(err).WithDebugf("provider name: %q, provider type: %q", p.Provider.GetResourceName(), p.GetSessionProviderType()) } } else { tokens = &oauth2.Token{AccessToken: sessionData.UpstreamAccessToken} @@ -270,13 +271,13 @@ func (p *FederationDomainResolvedOIDCIdentityProvider) UpstreamRefresh( if err != nil { return nil, resolvedprovider.ErrUpstreamRefreshError().WithHintf( "Upstream refresh returned an invalid ID token or UserInfo response.").WithTrace(err). - WithDebugf("provider name: %q, provider type: %q", p.Provider.GetName(), p.GetSessionProviderType()) + WithDebugf("provider name: %q, provider type: %q", p.Provider.GetResourceName(), p.GetSessionProviderType()) } mergedClaims := validatedTokens.IDToken.Claims // To the extent possible, check that the user's basic identity hasn't changed. We check that their downstream // username has not changed separately below, as part of reapplying the transformations. - err = validateUpstreamSubjectAndIssuerUnchangedSinceInitialLogin(mergedClaims, sessionData, p.Provider.GetName(), p.GetSessionProviderType()) + err = validateUpstreamSubjectAndIssuerUnchangedSinceInitialLogin(mergedClaims, sessionData, p.Provider.GetResourceName(), p.GetSessionProviderType()) if err != nil { return nil, err } @@ -292,7 +293,7 @@ func (p *FederationDomainResolvedOIDCIdentityProvider) UpstreamRefresh( if err != nil { return nil, resolvedprovider.ErrUpstreamRefreshError().WithHintf( "Upstream refresh error while extracting groups claim.").WithTrace(err). - WithDebugf("provider name: %q, provider type: %q", p.Provider.GetName(), p.GetSessionProviderType()) + WithDebugf("provider name: %q, provider type: %q", p.Provider.GetResourceName(), p.GetSessionProviderType()) } // It's possible that a username wasn't returned by the upstream provider during refresh, @@ -312,7 +313,9 @@ func (p *FederationDomainResolvedOIDCIdentityProvider) UpstreamRefresh( // overwriting the old one. if tokens.RefreshToken != "" { plog.Debug("upstream refresh request returned a new refresh token", - "providerName", p.Provider.GetName(), "providerType", p.GetSessionProviderType(), "providerUID", p.Provider.GetResourceUID()) + "identityProviderResourceName", p.Provider.GetResourceName(), + "identityProviderType", p.GetSessionProviderType(), + "identityProviderUID", p.Provider.GetResourceUID()) updatedSessionData.UpstreamRefreshToken = tokens.RefreshToken } @@ -370,11 +373,11 @@ func makeDownstreamOIDCSessionData( oidcUpstream upstreamprovider.UpstreamOIDCIdentityProviderI, token *oidctypes.Token, ) (*psession.OIDCSessionData, []string, error) { - upstreamSubject, err := extractStringClaimValue(oidc.IDTokenClaimSubject, oidcUpstream.GetName(), token.IDToken.Claims) + upstreamSubject, err := extractStringClaimValue(oidc.IDTokenClaimSubject, oidcUpstream.GetResourceName(), token.IDToken.Claims) if err != nil { return nil, nil, err } - upstreamIssuer, err := extractStringClaimValue(oidc.IDTokenClaimIssuer, oidcUpstream.GetName(), token.IDToken.Claims) + upstreamIssuer, err := extractStringClaimValue(oidc.IDTokenClaimIssuer, oidcUpstream.GetResourceName(), token.IDToken.Claims) if err != nil { return nil, nil, err } @@ -387,7 +390,7 @@ func makeDownstreamOIDCSessionData( const pleaseCheck = "please check configuration of OIDCIdentityProvider and the client in the " + "upstream provider's API/UI and try to get a refresh token if possible" logKV := []any{ - "upstreamName", oidcUpstream.GetName(), + "identityProviderResourceName", oidcUpstream.GetResourceName(), "scopes", oidcUpstream.GetScopes(), "additionalParams", oidcUpstream.GetAdditionalAuthcodeParams(), } @@ -452,7 +455,7 @@ func mapAdditionalClaimsFromUpstreamIDToken( if !ok { plog.Warning( "additionalClaims mapping claim in upstream ID token missing", - "upstreamName", upstreamIDPConfig.GetName(), + "identityProviderResourceName", upstreamIDPConfig.GetResourceName(), "claimName", upstreamClaimName, ) } else { @@ -469,11 +472,11 @@ func getDownstreamSubjectAndUpstreamUsernameFromUpstreamIDToken( ) (string, string, error) { // The spec says the "sub" claim is only unique per issuer, // so we will prepend the issuer string to make it globally unique. - upstreamIssuer, err := extractStringClaimValue(oidc.IDTokenClaimIssuer, upstreamIDPConfig.GetName(), idTokenClaims) + upstreamIssuer, err := extractStringClaimValue(oidc.IDTokenClaimIssuer, upstreamIDPConfig.GetResourceName(), idTokenClaims) if err != nil { return "", "", err } - upstreamSubject, err := extractStringClaimValue(oidc.IDTokenClaimSubject, upstreamIDPConfig.GetName(), idTokenClaims) + upstreamSubject, err := extractStringClaimValue(oidc.IDTokenClaimSubject, upstreamIDPConfig.GetResourceName(), idTokenClaims) if err != nil { return "", "", err } @@ -492,7 +495,7 @@ func getDownstreamSubjectAndUpstreamUsernameFromUpstreamIDToken( if !ok { plog.Warning( "username claim configured as \"email\" and upstream email_verified claim is not a boolean", - "upstreamName", upstreamIDPConfig.GetName(), + "identityProviderResourceName", upstreamIDPConfig.GetResourceName(), "configuredUsernameClaim", usernameClaimName, "emailVerifiedClaim", emailVerifiedAsInterface, ) @@ -501,14 +504,14 @@ func getDownstreamSubjectAndUpstreamUsernameFromUpstreamIDToken( if !emailVerified { plog.Warning( "username claim configured as \"email\" and upstream email_verified claim has false value", - "upstreamName", upstreamIDPConfig.GetName(), + "identityProviderResourceName", upstreamIDPConfig.GetResourceName(), "configuredUsernameClaim", usernameClaimName, ) return "", "", emailVerifiedClaimFalseErr } } - username, err := extractStringClaimValue(usernameClaimName, upstreamIDPConfig.GetName(), idTokenClaims) + username, err := extractStringClaimValue(usernameClaimName, upstreamIDPConfig.GetResourceName(), idTokenClaims) if err != nil { return "", "", err } @@ -571,7 +574,7 @@ func getGroupsFromUpstreamIDToken( if !ok { plog.Warning( "no groups claim in upstream ID token", - "upstreamName", upstreamIDPConfig.GetName(), + "identityProviderResourceName", upstreamIDPConfig.GetResourceName(), "configuredGroupsClaim", groupsClaimName, ) return nil, nil // the upstream IDP may have omitted the claim if the user has no groups @@ -581,7 +584,7 @@ func getGroupsFromUpstreamIDToken( if !okAsArray { plog.Warning( "groups claim in upstream ID token has invalid format", - "upstreamName", upstreamIDPConfig.GetName(), + "identityProviderResourceName", upstreamIDPConfig.GetResourceName(), "configuredGroupsClaim", groupsClaimName, ) return nil, requiredClaimInvalidFormatErr diff --git a/internal/federationdomain/upstreamprovider/upsteam_provider.go b/internal/federationdomain/upstreamprovider/upstream_provider.go similarity index 59% rename from internal/federationdomain/upstreamprovider/upsteam_provider.go rename to internal/federationdomain/upstreamprovider/upstream_provider.go index 40162c52a..5ca624892 100644 --- a/internal/federationdomain/upstreamprovider/upsteam_provider.go +++ b/internal/federationdomain/upstreamprovider/upstream_provider.go @@ -10,7 +10,9 @@ import ( "golang.org/x/oauth2" "k8s.io/apimachinery/pkg/types" + "go.pinniped.dev/generated/latest/apis/supervisor/idp/v1alpha1" "go.pinniped.dev/internal/authenticators" + "go.pinniped.dev/internal/setutil" "go.pinniped.dev/pkg/oidcclient/nonce" "go.pinniped.dev/pkg/oidcclient/oidctypes" "go.pinniped.dev/pkg/oidcclient/pkce" @@ -24,9 +26,9 @@ const ( AccessTokenType RevocableTokenType = "access_token" ) -// RefreshAttributes contains information about the user from the original login request +// LDAPRefreshAttributes contains information about the user from the original login request // and previous refreshes to be used during an LDAP session refresh. -type RefreshAttributes struct { +type LDAPRefreshAttributes struct { Username string Subject string DN string @@ -37,11 +39,11 @@ type RefreshAttributes struct { // UpstreamIdentityProviderI includes the interface functions that are common to all upstream identity provider types. // These represent the identity provider resources, i.e. OIDCIdentityProvider, etc. type UpstreamIdentityProviderI interface { - // GetName returns a name for this upstream provider. The controller watching the identity provider resources will + // GetResourceName returns a name for this upstream provider. The controller watching the identity provider resources will // set this to be the Name of the CR from its metadata. Note that this is different from the DisplayName configured // in each FederationDomain that uses this provider, so this name is for internal use only, not for interacting // with clients. Clients should not expect to see this name or send this name. - GetName() string + GetResourceName() string // GetResourceUID returns the Kubernetes resource ID GetResourceUID() types.UID @@ -123,5 +125,69 @@ type UpstreamLDAPIdentityProviderI interface { authenticators.UserAuthenticator // PerformRefresh performs a refresh against the upstream LDAP identity provider - PerformRefresh(ctx context.Context, storedRefreshAttributes RefreshAttributes, idpDisplayName string) (groups []string, err error) + PerformRefresh(ctx context.Context, storedRefreshAttributes LDAPRefreshAttributes, idpDisplayName string) (groups []string, err error) +} + +type GitHubUser struct { + Username string // could be login name, id, or login:id + Groups []string // could be names or slugs + DownstreamSubject string // the whole downstream subject URI +} + +// GitHubLoginDeniedError can be returned by UpstreamGithubIdentityProviderI GetUser() when a policy +// configured on GitHubIdentityProvider should prevent this user from completing authentication. +type GitHubLoginDeniedError struct { + message string +} + +func NewGitHubLoginDeniedError(message string) GitHubLoginDeniedError { + return GitHubLoginDeniedError{message: message} +} + +func (g GitHubLoginDeniedError) Error() string { + return g.message +} + +var _ error = &GitHubLoginDeniedError{} + +type UpstreamGithubIdentityProviderI interface { + UpstreamIdentityProviderI + + // GetClientID returns the OAuth client ID registered with the upstream provider to be used in the authorization code flow. + GetClientID() string + + // GetScopes returns the scopes to request in authorization (authcode or password grant) flow. + GetScopes() []string + + // GetUsernameAttribute returns the attribute from the GitHub API user response to use for the downstream username. + // See https://docs.github.com/en/rest/users/users?apiVersion=2022-11-28#get-the-authenticated-user. + // Note that this is a constructed value - do not expect that the result will exactly match one of the JSON fields. + GetUsernameAttribute() v1alpha1.GitHubUsernameAttribute + + // GetGroupNameAttribute returns the attribute from the GitHub API team response to use for the downstream group names. + // See https://docs.github.com/en/rest/teams/teams?apiVersion=2022-11-28#list-teams-for-the-authenticated-user. + // Note that this is a constructed value - do not expect that the result will exactly match one of the JSON fields. + GetGroupNameAttribute() v1alpha1.GitHubGroupNameAttribute + + // GetAllowedOrganizations returns a list of organizations configured to allow authentication. + // If this list has contents, a user must have membership in at least one of these organizations to log in, + // and only teams from the listed organizations should be represented as groups for the downstream token. + // If this list is empty, then any user can log in regardless of org membership, and any observable + // teams memberships should be represented as groups for the downstream token. + GetAllowedOrganizations() *setutil.CaseInsensitiveSet + + // GetAuthorizationURL returns the authorization URL for the configured GitHub. This will look like: + // https:///login/oauth/authorize + // It will not include any query parameters or fragment. Any subdomains or port will come from . + // It will never include a username or password in the authority section. + GetAuthorizationURL() string + + // ExchangeAuthcode performs an upstream GitHub authorization code exchange. + // Returns the raw access token. The access token expiry is not known. + ExchangeAuthcode(ctx context.Context, authcode string, redirectURI string) (string, error) + + // GetUser calls the user, orgs, and teams APIs of GitHub using the accessToken. + // It validates any required org memberships. It returns a User or an error. + // The IDP display name is passed to aid in building a suitable downstream subject string. + GetUser(ctx context.Context, accessToken string, idpDisplayName string) (*GitHubUser, error) } diff --git a/internal/fositestorage/accesstoken/accesstoken.go b/internal/fositestorage/accesstoken/accesstoken.go index eda7ce925..b114e3cc4 100644 --- a/internal/fositestorage/accesstoken/accesstoken.go +++ b/internal/fositestorage/accesstoken/accesstoken.go @@ -35,7 +35,8 @@ const ( // Version 5 is when we added the UpstreamUsername and UpstreamGroups fields to psession.CustomSessionData. // Version 6 is when we upgraded fosite in Dec 2023. // Version 7 is when OIDCClients were given configurable ID token lifetimes. - accessTokenStorageVersion = "7" + // Version 8 is when GitHubIdentityProvider was added. + accessTokenStorageVersion = "8" ) type RevocationStorage interface { diff --git a/internal/fositestorage/accesstoken/accesstoken_test.go b/internal/fositestorage/accesstoken/accesstoken_test.go index a20061e71..f0026bb20 100644 --- a/internal/fositestorage/accesstoken/accesstoken_test.go +++ b/internal/fositestorage/accesstoken/accesstoken_test.go @@ -30,7 +30,7 @@ import ( const ( namespace = "test-ns" - expectedVersion = "7" // update this when you update the storage version in the production code + expectedVersion = "8" // update this when you update the storage version in the production code ) var ( diff --git a/internal/fositestorage/authorizationcode/authorizationcode.go b/internal/fositestorage/authorizationcode/authorizationcode.go index 189a9ce35..c9dc80720 100644 --- a/internal/fositestorage/authorizationcode/authorizationcode.go +++ b/internal/fositestorage/authorizationcode/authorizationcode.go @@ -36,7 +36,8 @@ const ( // Version 5 is when we added the UpstreamUsername and UpstreamGroups fields to psession.CustomSessionData. // Version 6 is when we upgraded fosite in Dec 2023. // Version 7 is when OIDCClients were given configurable ID token lifetimes. - authorizeCodeStorageVersion = "7" + // Version 8 is when GitHubIdentityProvider was added. + authorizeCodeStorageVersion = "8" ) var _ fositeoauth2.AuthorizeCodeStorage = &authorizeCodeStorage{} @@ -265,7 +266,7 @@ const ExpectedAuthorizeCodeSessionJSONFromFuzzing = `{ ], "request_object_signing_alg": "廜+v,淬Ʋ4Dʧ呩锏緍场脋", "token_endpoint_auth_signing_alg": "ưƓǴ罷ǹ~]ea胠Ĺĩv絹b垇I", - "IDTokenLifetimeConfiguration":2.593156354696909e+18 + "IDTokenLifetimeConfiguration": 2593156354696908951 }, "scopes": [ "ǀŻQ'k頂箨J-", @@ -382,17 +383,20 @@ const ExpectedAuthorizeCodeSessionJSONFromFuzzing = `{ "IȽ齤士bEǎ": "跞@)¿,ɭS隑ip偶宾儮猷V麹", "ȝƋ鬯犦獢9c5¤.岵": "浛a齙\\蹼偦歛" } + }, + "github": { + "upstreamAccessToken": " 皦pSǬŝ社Vƅȭǝ*擦28Dž" } } }, "requestedAudience": [ - " 皦pSǬŝ社Vƅȭǝ*擦28Dž", - "vư" + "甍 ć\u003cʘ筫", + "蛖a³2ʫ承dʬ)ġ,TÀqy_" ], "grantedAudience": [ - "置b", - "筫MN\u0026錝D肁Ŷɽ蔒PR}Ųʓl{" + "$+溪ŸȢŒų崓ļ憽", + "姧骦:駝重EȫʆɵʮGɃ" ] }, - "version": "7" + "version": "8" }` diff --git a/internal/fositestorage/authorizationcode/authorizationcode_test.go b/internal/fositestorage/authorizationcode/authorizationcode_test.go index 3427303d1..06085f7a0 100644 --- a/internal/fositestorage/authorizationcode/authorizationcode_test.go +++ b/internal/fositestorage/authorizationcode/authorizationcode_test.go @@ -43,7 +43,7 @@ import ( const ( namespace = "test-ns" - expectedVersion = "7" // update this when you update the storage version in the production code + expectedVersion = "8" // update this when you update the storage version in the production code ) var ( diff --git a/internal/fositestorage/openidconnect/openidconnect.go b/internal/fositestorage/openidconnect/openidconnect.go index c5c7a4a2d..baadb94ec 100644 --- a/internal/fositestorage/openidconnect/openidconnect.go +++ b/internal/fositestorage/openidconnect/openidconnect.go @@ -36,7 +36,8 @@ const ( // Version 5 is when we added the UpstreamUsername and UpstreamGroups fields to psession.CustomSessionData. // Version 6 is when we upgraded fosite in Dec 2023. // Version 7 is when OIDCClients were given configurable ID token lifetimes. - oidcStorageVersion = "7" + // Version 8 is when GitHubIdentityProvider was added. + oidcStorageVersion = "8" ) var _ openid.OpenIDConnectRequestStorage = &openIDConnectRequestStorage{} diff --git a/internal/fositestorage/openidconnect/openidconnect_test.go b/internal/fositestorage/openidconnect/openidconnect_test.go index 8f64299ed..bcfd10f19 100644 --- a/internal/fositestorage/openidconnect/openidconnect_test.go +++ b/internal/fositestorage/openidconnect/openidconnect_test.go @@ -29,7 +29,7 @@ import ( const ( namespace = "test-ns" - expectedVersion = "7" // update this when you update the storage version in the production code + expectedVersion = "8" // update this when you update the storage version in the production code ) var ( diff --git a/internal/fositestorage/pkce/pkce.go b/internal/fositestorage/pkce/pkce.go index dda38208d..b36860cb1 100644 --- a/internal/fositestorage/pkce/pkce.go +++ b/internal/fositestorage/pkce/pkce.go @@ -34,7 +34,8 @@ const ( // Version 5 is when we added the UpstreamUsername and UpstreamGroups fields to psession.CustomSessionData. // Version 6 is when we upgraded fosite in Dec 2023. // Version 7 is when OIDCClients were given configurable ID token lifetimes. - pkceStorageVersion = "7" + // Version 8 is when GitHubIdentityProvider was added. + pkceStorageVersion = "8" ) var _ pkce.PKCERequestStorage = &pkceStorage{} diff --git a/internal/fositestorage/pkce/pkce_test.go b/internal/fositestorage/pkce/pkce_test.go index ed38d51f6..10fd61955 100644 --- a/internal/fositestorage/pkce/pkce_test.go +++ b/internal/fositestorage/pkce/pkce_test.go @@ -29,7 +29,7 @@ import ( const ( namespace = "test-ns" - expectedVersion = "7" // update this when you update the storage version in the production code + expectedVersion = "8" // update this when you update the storage version in the production code ) var ( diff --git a/internal/fositestorage/refreshtoken/refreshtoken.go b/internal/fositestorage/refreshtoken/refreshtoken.go index 0e74d7167..78986a2c0 100644 --- a/internal/fositestorage/refreshtoken/refreshtoken.go +++ b/internal/fositestorage/refreshtoken/refreshtoken.go @@ -35,7 +35,8 @@ const ( // Version 5 is when we added the UpstreamUsername and UpstreamGroups fields to psession.CustomSessionData. // Version 6 is when we upgraded fosite in Dec 2023. // Version 7 is when OIDCClients were given configurable ID token lifetimes. - refreshTokenStorageVersion = "7" + // Version 8 is when GitHubIdentityProvider was added. + refreshTokenStorageVersion = "8" ) type RevocationStorage interface { diff --git a/internal/fositestorage/refreshtoken/refreshtoken_test.go b/internal/fositestorage/refreshtoken/refreshtoken_test.go index 37718bd9c..2b96d8799 100644 --- a/internal/fositestorage/refreshtoken/refreshtoken_test.go +++ b/internal/fositestorage/refreshtoken/refreshtoken_test.go @@ -30,7 +30,7 @@ import ( const ( namespace = "test-ns" - expectedVersion = "7" // update this when you update the storage version in the production code + expectedVersion = "8" // update this when you update the storage version in the production code ) var ( diff --git a/internal/githubclient/githubclient.go b/internal/githubclient/githubclient.go new file mode 100644 index 000000000..36a7099e0 --- /dev/null +++ b/internal/githubclient/githubclient.go @@ -0,0 +1,235 @@ +// Copyright 2024 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package githubclient + +import ( + "context" + "errors" + "fmt" + "net/http" + "net/url" + "slices" + "strings" + + "github.com/google/go-github/v62/github" + "k8s.io/apimachinery/pkg/util/sets" + + "go.pinniped.dev/internal/plog" + "go.pinniped.dev/internal/setutil" +) + +const ( + emptyUserMeansTheAuthenticatedUser = "" + pageSize = 100 +) + +type UserInfo struct { + ID string + Login string +} + +type TeamInfo struct { + Name string + Slug string + Org string +} + +type GitHubInterface interface { + GetUserInfo(ctx context.Context) (*UserInfo, error) + GetOrgMembership(ctx context.Context) ([]string, error) + GetTeamMembership(ctx context.Context, allowedOrganizations *setutil.CaseInsensitiveSet) ([]TeamInfo, error) +} + +type githubClient struct { + client *github.Client +} + +var _ GitHubInterface = (*githubClient)(nil) + +func NewGitHubClient(httpClient *http.Client, apiBaseURL, token string) (GitHubInterface, error) { + const errorPrefix = "unable to build new github client" + + if httpClient == nil { + return nil, fmt.Errorf("%s: httpClient cannot be nil", errorPrefix) + } + + parsedURL, err := url.Parse(apiBaseURL) + if err != nil { + return nil, fmt.Errorf("%s: %w", errorPrefix, err) + } + + if !strings.HasSuffix(parsedURL.Path, "/") { + parsedURL.Path += "/" + } + + if parsedURL.Scheme != "https" { + return nil, fmt.Errorf(`%s: apiBaseURL must use "https" protocol, found %q instead`, errorPrefix, parsedURL.Scheme) + } + + if token == "" { + return nil, fmt.Errorf("%s: token cannot be empty string", errorPrefix) + } + + client := github.NewClient(httpClient).WithAuthToken(token) + client.BaseURL = parsedURL + + return &githubClient{ + client: client, + }, nil +} + +// GetUserInfo returns the "Login" and "ID" attributes of the logged-in user. +func (g *githubClient) GetUserInfo(ctx context.Context) (*UserInfo, error) { + const errorPrefix = "error fetching authenticated user" + + user, _, err := g.client.Users.Get(ctx, emptyUserMeansTheAuthenticatedUser) + if err != nil { + return nil, fmt.Errorf("%s: %w", errorPrefix, err) + } + if user == nil { // untested + return nil, fmt.Errorf("%s: user is nil", errorPrefix) + } + plog.Trace("got raw GitHub API user results", "user", user) + + userInfo := &UserInfo{ + Login: user.GetLogin(), + ID: fmt.Sprintf("%d", user.GetID()), + } + if userInfo.ID == "0" { + return nil, fmt.Errorf(`%s: the "id" attribute is missing`, errorPrefix) + } + if userInfo.Login == "" { + return nil, fmt.Errorf(`%s: the "login" attribute is missing`, errorPrefix) + } + + plog.Trace("calculated response from GitHub user endpoint", "user", userInfo) + return userInfo, nil +} + +// GetOrgMembership returns an array of the "Login" attributes for all organizations to which the authenticated user belongs. +func (g *githubClient) GetOrgMembership(ctx context.Context) ([]string, error) { + const errorPrefix = "error fetching organizations for authenticated user" + + organizationLogins := sets.New[string]() + + opt := &github.ListOptions{PerPage: pageSize} + // get all pages of results + for { + organizationResults, response, err := g.client.Organizations.List(ctx, emptyUserMeansTheAuthenticatedUser, opt) + if err != nil { + return nil, fmt.Errorf("%s: %w", errorPrefix, err) + } + plog.Trace("got raw GitHub API org results", "orgs", organizationResults, "hasNextPage", response.NextPage) + + for _, organization := range organizationResults { + organizationLogins.Insert(organization.GetLogin()) + } + if response.NextPage == 0 { + break + } + opt.Page = response.NextPage + } + + if organizationLogins.Has("") { + return nil, fmt.Errorf(`%s: one or more organizations is missing the "login" attribute`, errorPrefix) + } + + plog.Trace("calculated response from GitHub org membership endpoint", "orgs", organizationLogins.UnsortedList()) + return organizationLogins.UnsortedList(), nil +} + +func isOrgAllowed(allowedOrganizations *setutil.CaseInsensitiveSet, login string) bool { + return allowedOrganizations.Empty() || allowedOrganizations.ContainsIgnoringCase(login) +} + +func buildAndValidateParentTeam(githubTeam *github.Team, organizationLogin string) (*TeamInfo, error) { + return buildTeam(githubTeam, organizationLogin) +} + +func buildAndValidateTeam(githubTeam *github.Team) (*TeamInfo, error) { + if githubTeam.GetOrganization() == nil { + return nil, errors.New(`missing the "organization" attribute for a team`) + } + organizationLogin := githubTeam.GetOrganization().GetLogin() + if organizationLogin == "" { + return nil, errors.New(`missing the organization's "login" attribute for a team`) + } + + return buildTeam(githubTeam, organizationLogin) +} + +func buildTeam(githubTeam *github.Team, organizationLogin string) (*TeamInfo, error) { + teamInfo := &TeamInfo{ + Name: githubTeam.GetName(), + Slug: githubTeam.GetSlug(), + Org: organizationLogin, + } + if teamInfo.Name == "" { + return nil, errors.New(`the "name" attribute is missing for a team`) + } + if teamInfo.Slug == "" { + return nil, errors.New(`the "slug" attribute is missing for a team`) + } + return teamInfo, nil +} + +// GetTeamMembership returns a description of each team to which the authenticated user belongs. +// If allowedOrganizations is not empty, will filter the results to only those teams which belong to the allowed organizations. +// Parent teams will also be returned. +func (g *githubClient) GetTeamMembership(ctx context.Context, allowedOrganizations *setutil.CaseInsensitiveSet) ([]TeamInfo, error) { + const errorPrefix = "error fetching team membership for authenticated user" + teamInfos := sets.New[TeamInfo]() + + opt := &github.ListOptions{PerPage: pageSize} + // get all pages of results + for { + teamsResults, response, err := g.client.Teams.ListUserTeams(ctx, opt) + if err != nil { + return nil, fmt.Errorf("%s: %w", errorPrefix, err) + } + plog.Trace("got raw GitHub API team results", "teams", teamsResults, "hasNextPage", response.NextPage) + + for _, team := range teamsResults { + teamInfo, err := buildAndValidateTeam(team) + if err != nil { + return nil, fmt.Errorf("%s: %w", errorPrefix, err) + } + + if !isOrgAllowed(allowedOrganizations, teamInfo.Org) { + continue + } + + teamInfos.Insert(*teamInfo) + + parent := team.GetParent() + if parent != nil { + // The GitHub API does not return the Organization for the Parent of the team. + // Use the org of the child as the org of the parent, since they must come from the same org. + parentTeamInfo, err := buildAndValidateParentTeam(parent, teamInfo.Org) + if err != nil { + return nil, fmt.Errorf("%s: %w", errorPrefix, err) + } + + teamInfos.Insert(*parentTeamInfo) + } + } + if response.NextPage == 0 { + break + } + opt.Page = response.NextPage + } + + // Sort by org and then by name, just so we always return teams in the same order. + sortedTeams := teamInfos.UnsortedList() + slices.SortStableFunc(sortedTeams, func(a, b TeamInfo) int { + orgsCompared := strings.Compare(a.Org, b.Org) + if orgsCompared == 0 { + return strings.Compare(a.Slug, b.Slug) + } + return orgsCompared + }) + + plog.Trace("calculated response from GitHub teams endpoint", "teams", sortedTeams) + return sortedTeams, nil +} diff --git a/internal/githubclient/githubclient_test.go b/internal/githubclient/githubclient_test.go new file mode 100644 index 000000000..ae3b00eba --- /dev/null +++ b/internal/githubclient/githubclient_test.go @@ -0,0 +1,833 @@ +// Copyright 2024 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package githubclient + +import ( + "context" + "net/http" + "strings" + "testing" + + "github.com/google/go-github/v62/github" + "github.com/migueleliasweb/go-github-mock/src/mock" + "github.com/stretchr/testify/require" + "k8s.io/client-go/util/cert" + + "go.pinniped.dev/internal/net/phttp" + "go.pinniped.dev/internal/setutil" + "go.pinniped.dev/internal/testutil/tlsserver" +) + +func TestNewGitHubClient(t *testing.T) { + t.Parallel() + + t.Run("rejects nil http client", func(t *testing.T) { + _, err := NewGitHubClient(nil, "https://api.github.com/", "") + require.EqualError(t, err, "unable to build new github client: httpClient cannot be nil") + }) + + tests := []struct { + name string + apiBaseURL string + token string + wantBaseURL string + wantErr string + }{ + { + name: "happy path with https://api.github.com/", + apiBaseURL: "https://api.github.com/", + token: "some-token", + wantBaseURL: "https://api.github.com/", + }, + { + name: "adds trailing slash to path for https://api.github.com", + apiBaseURL: "https://api.github.com", + token: "other-token", + wantBaseURL: "https://api.github.com/", + }, + { + name: "adds trailing slash to path for Enterprise URL https://fake.enterprise.tld/api/v3", + apiBaseURL: "https://fake.enterprise.tld/api/v3", + token: "some-enterprise-token", + wantBaseURL: "https://fake.enterprise.tld/api/v3/", + }, + { + name: "rejects apiBaseURL without https:// scheme", + apiBaseURL: "scp://github.com", + token: "some-token", + wantErr: `unable to build new github client: apiBaseURL must use "https" protocol, found "scp" instead`, + }, + { + name: "rejects apiBaseURL with empty scheme", + apiBaseURL: "github.com", + token: "some-token", + wantErr: `unable to build new github client: apiBaseURL must use "https" protocol, found "" instead`, + }, + { + name: "rejects empty token", + apiBaseURL: "https://api.github.com/", + wantErr: "unable to build new github client: token cannot be empty string", + }, + { + name: "returns errors from url.Parse", + apiBaseURL: "https:// example.com", + token: "some-token", + wantErr: `unable to build new github client: parse "https:// example.com": invalid character " " in host name`, + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + t.Parallel() + + called := false + testServer, testServerCA := tlsserver.TestServerIPv4(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + require.Len(t, r.Header["Authorization"], 1) + require.Equal(t, "Bearer "+test.token, r.Header.Get("Authorization")) + called = true + }), nil) + + t.Cleanup(func() { + require.True(t, (test.wantErr == "" && called) || (test.wantErr != "" && !called)) + }) + + pool, err := cert.NewPoolFromBytes(testServerCA) + require.NoError(t, err) + + httpClient := phttp.Default(pool) + + actualI, err := NewGitHubClient(httpClient, test.apiBaseURL, test.token) + + if test.wantErr != "" { + require.EqualError(t, err, test.wantErr) + } else { + require.NoError(t, err) + + require.NotNil(t, actualI) + actual, ok := actualI.(*githubClient) + require.True(t, ok) + require.NotNil(t, actual.client.BaseURL) + require.Equal(t, test.wantBaseURL, actual.client.BaseURL.String()) + + // Force the githubClient's httpClient roundTrippers to run and add the Authorization header + + req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, testServer.URL, nil) + require.NoError(t, err) + + _, err = actual.client.Client().Do(req) //nolint:bodyclose + require.NoError(t, err) + } + }) + } +} + +func TestGetUser(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + httpClient *http.Client + token string + ctx context.Context + wantErr string + wantUserInfo UserInfo + }{ + { + name: "happy path", + httpClient: mock.NewMockedHTTPClient( + mock.WithRequestMatch( + mock.GetUser, + github.User{ + Login: github.String("some-username"), + ID: github.Int64(12345678), + }, + ), + ), + token: "some-token", + wantUserInfo: UserInfo{ + Login: "some-username", + ID: "12345678", + }, + }, + { + name: "the token is added in the Authorization header", + httpClient: mock.NewMockedHTTPClient( + mock.WithRequestMatchHandler( + mock.GetUser, + http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + require.Len(t, r.Header["Authorization"], 1) + require.Equal(t, "Bearer does-this-token-work", r.Header.Get("Authorization")) + _, err := w.Write([]byte(`{"login":"some-authenticated-username","id":999888}`)) + require.NoError(t, err) + }), + ), + ), + token: "does-this-token-work", + wantUserInfo: UserInfo{ + Login: "some-authenticated-username", + ID: "999888", + }, + }, + { + name: "handles missing login", + httpClient: mock.NewMockedHTTPClient( + mock.WithRequestMatch( + mock.GetUser, + github.User{ + ID: github.Int64(12345678), + }, + ), + ), + token: "does-this-token-work", + wantErr: `error fetching authenticated user: the "login" attribute is missing`, + }, + { + name: "handles missing ID", + httpClient: mock.NewMockedHTTPClient( + mock.WithRequestMatch( + mock.GetUser, + github.User{ + Login: github.String("some-username"), + }, + ), + ), + token: "does-this-token-work", + wantErr: `error fetching authenticated user: the "id" attribute is missing`, + }, + { + name: "passes the context parameter into the API call", + token: "some-token", + httpClient: mock.NewMockedHTTPClient(), + ctx: func() context.Context { + canceledCtx, cancel := context.WithCancel(context.Background()) + cancel() + return canceledCtx + }(), + wantErr: "error fetching authenticated user: context canceled", + }, + { + name: "returns errors from the API", + httpClient: mock.NewMockedHTTPClient( + mock.WithRequestMatchHandler( + mock.GetUser, + http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + mock.WriteError( + w, + http.StatusInternalServerError, + "internal server error from the server", + ) + }), + ), + ), + token: "some-token", + wantErr: "error fetching authenticated user: GET {SERVER_URL}/user: 500 internal server error from the server []", + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + t.Parallel() + + githubClient := &githubClient{ + client: github.NewClient(test.httpClient).WithAuthToken(test.token), + } + + ctx := context.Background() + if test.ctx != nil { + ctx = test.ctx + } + + actual, err := githubClient.GetUserInfo(ctx) + if test.wantErr != "" { + rt, ok := test.httpClient.Transport.(*mock.EnforceHostRoundTripper) + require.True(t, ok) + test.wantErr = strings.ReplaceAll(test.wantErr, "{SERVER_URL}", rt.Host) + require.EqualError(t, err, test.wantErr) + } else { + require.NoError(t, err) + require.NotNil(t, actual) + require.Equal(t, test.wantUserInfo, *actual) + } + }) + } +} + +func TestGetOrgMembership(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + httpClient *http.Client + token string + ctx context.Context + wantErr string + wantOrgs []string + }{ + { + name: "happy path", + httpClient: mock.NewMockedHTTPClient( + mock.WithRequestMatch( + mock.GetUserOrgs, + []github.Organization{ + {Login: github.String("org1")}, + {Login: github.String("org2")}, + {Login: github.String("org3")}, + }, + ), + ), + token: "some-token", + wantOrgs: []string{"org1", "org2", "org3"}, + }, + { + name: "happy path with pagination", + httpClient: mock.NewMockedHTTPClient( + mock.WithRequestMatchPages( + mock.GetUserOrgs, + []github.Organization{ + {Login: github.String("page1-org1")}, + {Login: github.String("page1-org2")}, + {Login: github.String("page1-org3")}, + }, + []github.Organization{ + {Login: github.String("page2-org1")}, + {Login: github.String("page2-org2")}, + {Login: github.String("page2-org3")}, + }, + ), + ), + token: "some-token", + wantOrgs: []string{"page1-org1", "page1-org2", "page1-org3", "page2-org1", "page2-org2", "page2-org3"}, + }, + { + name: "the token is added in the Authorization header", + httpClient: mock.NewMockedHTTPClient( + mock.WithRequestMatchHandler( + mock.GetUserOrgs, + http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + require.Len(t, r.Header["Authorization"], 1) + require.Equal(t, "Bearer does-this-token-work", r.Header.Get("Authorization")) + _, err := w.Write([]byte(`[{"login":"some-org-to-which-the-authenticated-user-belongs"}]`)) + require.NoError(t, err) + }), + ), + ), + token: "does-this-token-work", + wantOrgs: []string{"some-org-to-which-the-authenticated-user-belongs"}, + }, + { + name: "errors when a Login field is empty", + httpClient: mock.NewMockedHTTPClient( + mock.WithRequestMatch( + mock.GetUserOrgs, + []github.Organization{ + {Login: github.String("page1-org1")}, + {Login: nil}, + {Login: github.String("page1-org3")}, + }, + ), + ), + token: "some-token", + wantErr: `error fetching organizations for authenticated user: one or more organizations is missing the "login" attribute`, + }, + { + name: "passes the context parameter into the API call", + token: "some-token", + httpClient: mock.NewMockedHTTPClient(), + ctx: func() context.Context { + canceledCtx, cancel := context.WithCancel(context.Background()) + cancel() + return canceledCtx + }(), + wantErr: "error fetching organizations for authenticated user: context canceled", + }, + { + name: "returns errors from the API", + httpClient: mock.NewMockedHTTPClient( + mock.WithRequestMatchHandler( + mock.GetUserOrgs, + http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + mock.WriteError( + w, + http.StatusFailedDependency, + "some random client error", + ) + }), + ), + ), + token: "some-token", + wantErr: "error fetching organizations for authenticated user: GET {SERVER_URL}/user/orgs?per_page=100: 424 some random client error []", + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + t.Parallel() + + githubClient := &githubClient{ + client: github.NewClient(test.httpClient).WithAuthToken(test.token), + } + + ctx := context.Background() + if test.ctx != nil { + ctx = test.ctx + } + + actual, err := githubClient.GetOrgMembership(ctx) + if test.wantErr != "" { + rt, ok := test.httpClient.Transport.(*mock.EnforceHostRoundTripper) + require.True(t, ok) + test.wantErr = strings.ReplaceAll(test.wantErr, "{SERVER_URL}", rt.Host) + require.EqualError(t, err, test.wantErr) + return + } + + require.NotNil(t, actual) + require.ElementsMatch(t, test.wantOrgs, actual) + }) + } +} + +func TestGetTeamMembership(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + httpClient *http.Client + token string + ctx context.Context + allowedOrganizations *setutil.CaseInsensitiveSet + wantErr string + wantTeams []TeamInfo + }{ + { + name: "happy path", + httpClient: mock.NewMockedHTTPClient( + mock.WithRequestMatch( + mock.GetUserTeams, + []github.Team{ + { + Name: github.String("orgAlpha-team1-name"), + Slug: github.String("orgAlpha-team1-slug"), + Organization: &github.Organization{ + Login: github.String("alpha"), + }, + }, + { + Name: github.String("orgAlpha-team2-name"), + Slug: github.String("orgAlpha-team2-slug"), + Organization: &github.Organization{ + Login: github.String("alpha"), + }, + }, + { + Name: github.String("orgAlpha-team3-name"), + Slug: github.String("orgAlpha-team3-slug"), + Organization: &github.Organization{ + Login: github.String("alpha"), + }, + }, + { + Name: github.String("orgBeta-team1-name"), + Slug: github.String("orgBeta-team1-slug"), + Organization: &github.Organization{ + Login: github.String("beta"), + }, + }, + }, + ), + ), + token: "some-token", + allowedOrganizations: setutil.NewCaseInsensitiveSet("alpha", "beta"), + wantTeams: []TeamInfo{ + { + Name: "orgAlpha-team1-name", + Slug: "orgAlpha-team1-slug", + Org: "alpha", + }, + { + Name: "orgAlpha-team2-name", + Slug: "orgAlpha-team2-slug", + Org: "alpha", + }, + { + Name: "orgAlpha-team3-name", + Slug: "orgAlpha-team3-slug", + Org: "alpha", + }, + { + Name: "orgBeta-team1-name", + Slug: "orgBeta-team1-slug", + Org: "beta", + }, + }, + }, + { + name: "filters by allowedOrganizations in a case-insensitive way, but preserves case as returned by GitHub API in the result", + httpClient: mock.NewMockedHTTPClient( + mock.WithRequestMatch( + mock.GetUserTeams, + []github.Team{ + { + Name: github.String("team1-name"), + Slug: github.String("team1-slug"), + Organization: &github.Organization{ + Login: github.String("alPhA"), + }, + }, + { + Name: github.String("team2-name"), + Slug: github.String("team2-slug"), + Organization: &github.Organization{ + Login: github.String("bEtA"), + }, + }, + { + Name: github.String("team3-name"), + Slug: github.String("team3-slug"), + Organization: &github.Organization{ + Login: github.String("gAmmA"), + }, + }, + }, + ), + ), + token: "some-token", + allowedOrganizations: setutil.NewCaseInsensitiveSet("ALPHA", "gamma"), + wantTeams: []TeamInfo{ + { + Name: "team1-name", + Slug: "team1-slug", + Org: "alPhA", + }, + { + Name: "team3-name", + Slug: "team3-slug", + Org: "gAmmA", + }, + }, + }, + { + name: "when allowedOrganizations is empty, return all teams", + httpClient: mock.NewMockedHTTPClient( + mock.WithRequestMatch( + mock.GetUserTeams, + []github.Team{ + { + Name: github.String("team1-name"), + Slug: github.String("team1-slug"), + Organization: &github.Organization{ + Login: github.String("alpha"), + }, + }, + { + Name: github.String("team2-name"), + Slug: github.String("team2-slug"), + Organization: &github.Organization{ + Login: github.String("beta"), + }, + }, + { + Name: github.String("team3-name"), + Slug: github.String("team3-slug"), + Parent: &github.Team{ + Name: github.String("delta-team-name"), + Slug: github.String("delta-team-slug"), + Organization: nil, // the real GitHub API does not return Org on "Parent" team. + }, + Organization: &github.Organization{ + Login: github.String("gamma"), + }, + }, + }, + ), + ), + token: "some-token", + wantTeams: []TeamInfo{ + { + Name: "team1-name", + Slug: "team1-slug", + Org: "alpha", + }, + { + Name: "team2-name", + Slug: "team2-slug", + Org: "beta", + }, + { + Name: "delta-team-name", + Slug: "delta-team-slug", + Org: "gamma", + }, + { + Name: "team3-name", + Slug: "team3-slug", + Org: "gamma", + }, + }, + }, + { + name: "includes parent team in allowed orgs if present", + httpClient: mock.NewMockedHTTPClient( + mock.WithRequestMatch( + mock.GetUserTeams, + []github.Team{ + { + Name: github.String("team-name-with-parent"), + Slug: github.String("team-slug-with-parent"), + Parent: &github.Team{ + Name: github.String("parent-team-name"), + Slug: github.String("parent-team-slug"), + Organization: nil, // the real GitHub API does not return Org on "Parent" team. + }, + Organization: &github.Organization{ + Login: github.String("org-with-nested-teams"), + }, + }, + { + Name: github.String("team-name-with-same-parent-again"), + Slug: github.String("team-slug-with-same-parent-again"), + Parent: &github.Team{ + Name: github.String("parent-team-name"), + Slug: github.String("parent-team-slug"), + Organization: nil, // the real GitHub API does not return Org on "Parent" team. + }, + Organization: &github.Organization{ + Login: github.String("org-with-nested-teams"), + }, + }, + { + Name: github.String("parent-team-name"), + Slug: github.String("parent-team-slug"), + Organization: &github.Organization{ + Login: github.String("org-with-nested-teams"), + }, + }, + { + Name: github.String("team-name-with-parent-from-disallowed-org"), + Slug: github.String("team-slug-with-parent-from-disallowed-org"), + Parent: &github.Team{ + Name: github.String("parent-team-name-from-disallowed-org"), + Slug: github.String("parent-team-slug-from-disallowed-org"), + Organization: nil, // the real GitHub API does not return Org on "Parent" team. + }, + Organization: &github.Organization{ + Login: github.String("disallowed-org"), + }, + }, + { + Name: github.String("team-name-without-parent"), + Slug: github.String("team-slug-without-parent"), + Organization: &github.Organization{ + Login: github.String("beta"), + }, + }, + }, + ), + ), + token: "some-token", + allowedOrganizations: setutil.NewCaseInsensitiveSet("org-with-nested-teams", "beta"), + wantTeams: []TeamInfo{ + { + Name: "team-name-without-parent", + Slug: "team-slug-without-parent", + Org: "beta", + }, + { + Name: "parent-team-name", + Slug: "parent-team-slug", + Org: "org-with-nested-teams", + }, + { + Name: "team-name-with-parent", + Slug: "team-slug-with-parent", + Org: "org-with-nested-teams", + }, + { + Name: "team-name-with-same-parent-again", + Slug: "team-slug-with-same-parent-again", + Org: "org-with-nested-teams", + }, + }, + }, + { + name: "happy path with pagination", + httpClient: mock.NewMockedHTTPClient( + mock.WithRequestMatchPages( + mock.GetUserTeams, + []github.Team{ + { + Name: github.String("page1-team-name"), + Slug: github.String("page1-team-slug"), + Organization: &github.Organization{ + Login: github.String("page1-org-name"), + }, + }, + }, + []github.Team{ + { + Name: github.String("page2-team-name"), + Slug: github.String("page2-team-slug"), + Organization: &github.Organization{ + Login: github.String("page2-org-name"), + }, + }, + }, + ), + ), + token: "some-token", + allowedOrganizations: setutil.NewCaseInsensitiveSet("page1-org-name", "page2-org-name"), + wantTeams: []TeamInfo{ + { + Name: "page1-team-name", + Slug: "page1-team-slug", + Org: "page1-org-name", + }, + { + Name: "page2-team-name", + Slug: "page2-team-slug", + Org: "page2-org-name", + }, + }, + }, + { + name: "missing organization attribute returns an error", + httpClient: mock.NewMockedHTTPClient( + mock.WithRequestMatch( + mock.GetUserTeams, + []github.Team{ + { + Name: github.String("team-name"), + Slug: github.String("team-slug"), + }, + }, + ), + ), + wantErr: `error fetching team membership for authenticated user: missing the "organization" attribute for a team`, + }, + { + name: "missing organization's login attribute returns an error", + httpClient: mock.NewMockedHTTPClient( + mock.WithRequestMatch( + mock.GetUserTeams, + []github.Team{ + { + Name: github.String("team-name"), + Slug: github.String("team-slug"), + Organization: &github.Organization{}, + }, + }, + ), + ), + wantErr: `error fetching team membership for authenticated user: missing the organization's "login" attribute for a team`, + }, + { + name: "missing the name attribute for a team returns an error", + httpClient: mock.NewMockedHTTPClient( + mock.WithRequestMatch( + mock.GetUserTeams, + []github.Team{ + { + Slug: github.String("team-slug"), + Organization: &github.Organization{ + Login: github.String("some-org"), + }, + }, + }, + ), + ), + wantErr: `error fetching team membership for authenticated user: the "name" attribute is missing for a team`, + }, + { + name: "missing the slug attribute for a team returns an error", + httpClient: mock.NewMockedHTTPClient( + mock.WithRequestMatch( + mock.GetUserTeams, + []github.Team{ + { + Name: github.String("team-name"), + Organization: &github.Organization{ + Login: github.String("some-org"), + }, + }, + }, + ), + ), + wantErr: `error fetching team membership for authenticated user: the "slug" attribute is missing for a team`, + }, + { + name: "the token is added in the Authorization header", + httpClient: mock.NewMockedHTTPClient( + mock.WithRequestMatchHandler( + mock.GetUserTeams, + http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + require.Len(t, r.Header["Authorization"], 1) + require.Equal(t, "Bearer does-this-token-work", r.Header.Get("Authorization")) + _, err := w.Write([]byte(`[{"name":"team1-name","slug":"team1-slug","organization":{"login":"org-login"}}]`)) + require.NoError(t, err) + }), + ), + ), + token: "does-this-token-work", + allowedOrganizations: setutil.NewCaseInsensitiveSet("org-login"), + wantTeams: []TeamInfo{ + { + Name: "team1-name", + Slug: "team1-slug", + Org: "org-login", + }, + }, + }, + { + name: "passes the context parameter into the API call", + token: "some-token", + httpClient: mock.NewMockedHTTPClient(), + ctx: func() context.Context { + canceledCtx, cancel := context.WithCancel(context.Background()) + cancel() + return canceledCtx + }(), + wantErr: "error fetching team membership for authenticated user: context canceled", + }, + { + name: "returns errors from the API", + httpClient: mock.NewMockedHTTPClient( + mock.WithRequestMatchHandler( + mock.GetUserTeams, + http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + mock.WriteError( + w, + http.StatusFailedDependency, + "some random client error", + ) + }), + ), + ), + token: "some-token", + wantErr: "error fetching team membership for authenticated user: GET {SERVER_URL}/user/teams?per_page=100: 424 some random client error []", + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + t.Parallel() + + githubClient := &githubClient{ + client: github.NewClient(test.httpClient).WithAuthToken(test.token), + } + + ctx := context.Background() + if test.ctx != nil { + ctx = test.ctx + } + + actual, err := githubClient.GetTeamMembership(ctx, test.allowedOrganizations) + if test.wantErr != "" { + rt, ok := test.httpClient.Transport.(*mock.EnforceHostRoundTripper) + require.True(t, ok) + test.wantErr = strings.ReplaceAll(test.wantErr, "{SERVER_URL}", rt.Host) + require.EqualError(t, err, test.wantErr) + } else { + require.NoError(t, err) + require.NotNil(t, actual) + require.Equal(t, test.wantTeams, actual) + } + }) + } +} diff --git a/internal/mocks/mockgithubclient/generate.go b/internal/mocks/mockgithubclient/generate.go new file mode 100644 index 000000000..8bca3dd91 --- /dev/null +++ b/internal/mocks/mockgithubclient/generate.go @@ -0,0 +1,6 @@ +// Copyright 2024 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package mockgithubclient + +//go:generate go run -v go.uber.org/mock/mockgen -destination=mockgithubclient.go -package=mockgithubclient -copyright_file=../../../hack/header.txt go.pinniped.dev/internal/githubclient GitHubInterface diff --git a/internal/mocks/mockgithubclient/mockgithubclient.go b/internal/mocks/mockgithubclient/mockgithubclient.go new file mode 100644 index 000000000..d259daadf --- /dev/null +++ b/internal/mocks/mockgithubclient/mockgithubclient.go @@ -0,0 +1,91 @@ +// Copyright 2020-2024 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +// + +// Code generated by MockGen. DO NOT EDIT. +// Source: go.pinniped.dev/internal/githubclient (interfaces: GitHubInterface) +// +// Generated by this command: +// +// mockgen -destination=mockgithubclient.go -package=mockgithubclient -copyright_file=../../../hack/header.txt go.pinniped.dev/internal/githubclient GitHubInterface +// + +// Package mockgithubclient is a generated GoMock package. +package mockgithubclient + +import ( + context "context" + reflect "reflect" + + githubclient "go.pinniped.dev/internal/githubclient" + setutil "go.pinniped.dev/internal/setutil" + gomock "go.uber.org/mock/gomock" +) + +// MockGitHubInterface is a mock of GitHubInterface interface. +type MockGitHubInterface struct { + ctrl *gomock.Controller + recorder *MockGitHubInterfaceMockRecorder +} + +// MockGitHubInterfaceMockRecorder is the mock recorder for MockGitHubInterface. +type MockGitHubInterfaceMockRecorder struct { + mock *MockGitHubInterface +} + +// NewMockGitHubInterface creates a new mock instance. +func NewMockGitHubInterface(ctrl *gomock.Controller) *MockGitHubInterface { + mock := &MockGitHubInterface{ctrl: ctrl} + mock.recorder = &MockGitHubInterfaceMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockGitHubInterface) EXPECT() *MockGitHubInterfaceMockRecorder { + return m.recorder +} + +// GetOrgMembership mocks base method. +func (m *MockGitHubInterface) GetOrgMembership(arg0 context.Context) ([]string, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetOrgMembership", arg0) + ret0, _ := ret[0].([]string) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetOrgMembership indicates an expected call of GetOrgMembership. +func (mr *MockGitHubInterfaceMockRecorder) GetOrgMembership(arg0 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetOrgMembership", reflect.TypeOf((*MockGitHubInterface)(nil).GetOrgMembership), arg0) +} + +// GetTeamMembership mocks base method. +func (m *MockGitHubInterface) GetTeamMembership(arg0 context.Context, arg1 *setutil.CaseInsensitiveSet) ([]githubclient.TeamInfo, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetTeamMembership", arg0, arg1) + ret0, _ := ret[0].([]githubclient.TeamInfo) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetTeamMembership indicates an expected call of GetTeamMembership. +func (mr *MockGitHubInterfaceMockRecorder) GetTeamMembership(arg0, arg1 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetTeamMembership", reflect.TypeOf((*MockGitHubInterface)(nil).GetTeamMembership), arg0, arg1) +} + +// GetUserInfo mocks base method. +func (m *MockGitHubInterface) GetUserInfo(arg0 context.Context) (*githubclient.UserInfo, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetUserInfo", arg0) + ret0, _ := ret[0].(*githubclient.UserInfo) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetUserInfo indicates an expected call of GetUserInfo. +func (mr *MockGitHubInterfaceMockRecorder) GetUserInfo(arg0 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetUserInfo", reflect.TypeOf((*MockGitHubInterface)(nil).GetUserInfo), arg0) +} diff --git a/internal/mocks/mockupstreamoidcidentityprovider/mockupstreamoidcidentityprovider.go b/internal/mocks/mockupstreamoidcidentityprovider/mockupstreamoidcidentityprovider.go index 1c00c427e..5d5483060 100644 --- a/internal/mocks/mockupstreamoidcidentityprovider/mockupstreamoidcidentityprovider.go +++ b/internal/mocks/mockupstreamoidcidentityprovider/mockupstreamoidcidentityprovider.go @@ -149,18 +149,18 @@ func (mr *MockUpstreamOIDCIdentityProviderIMockRecorder) GetGroupsClaim() *gomoc return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetGroupsClaim", reflect.TypeOf((*MockUpstreamOIDCIdentityProviderI)(nil).GetGroupsClaim)) } -// GetName mocks base method. -func (m *MockUpstreamOIDCIdentityProviderI) GetName() string { +// GetResourceName mocks base method. +func (m *MockUpstreamOIDCIdentityProviderI) GetResourceName() string { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetName") + ret := m.ctrl.Call(m, "GetResourceName") ret0, _ := ret[0].(string) return ret0 } -// GetName indicates an expected call of GetName. -func (mr *MockUpstreamOIDCIdentityProviderIMockRecorder) GetName() *gomock.Call { +// GetResourceName indicates an expected call of GetResourceName. +func (mr *MockUpstreamOIDCIdentityProviderIMockRecorder) GetResourceName() *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetName", reflect.TypeOf((*MockUpstreamOIDCIdentityProviderI)(nil).GetName)) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetResourceName", reflect.TypeOf((*MockUpstreamOIDCIdentityProviderI)(nil).GetResourceName)) } // GetResourceUID mocks base method. diff --git a/internal/plog/plog.go b/internal/plog/plog.go index 9a7d7371d..085416cf6 100644 --- a/internal/plog/plog.go +++ b/internal/plog/plog.go @@ -12,6 +12,7 @@ // // info should be reserved for "nice to know" information. It should be possible to run a production // pinniped server at the info log level with no performance degradation due to high log volume. +// // debug should be used for information targeted at developers and to aid in support cases. Care must // be taken at this level to not leak any secrets into the log stream. That is, even though debug may // cause performance issues in production, it must not cause security issues in production. diff --git a/internal/plog/plog_test.go b/internal/plog/plog_test.go index 1359c23aa..44a407c1d 100644 --- a/internal/plog/plog_test.go +++ b/internal/plog/plog_test.go @@ -241,7 +241,7 @@ func TestPlog(t *testing.T) { testAllPlogMethods(l.withDepth(-2)) }, want: ` -{"level":"error","timestamp":"2099-08-08T13:57:36.123456Z","caller":"logr@v1.4.1/logr.go:$logr.Logger.Error","message":"e","panda":2,"error":"some err"} +{"level":"error","timestamp":"2099-08-08T13:57:36.123456Z","caller":"logr@v1.4.2/logr.go:$logr.Logger.Error","message":"e","panda":2,"error":"some err"} {"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","caller":"plog/plog.go:$plog.pLogger.warningDepth","message":"w","warning":true,"panda":2} {"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","caller":"plog/plog.go:$plog.pLogger.warningDepth","message":"we","warning":true,"error":"some err","panda":2} {"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","caller":"plog/plog.go:$plog.pLogger.infoDepth","message":"i","panda":2} @@ -250,8 +250,8 @@ func TestPlog(t *testing.T) { {"level":"debug","timestamp":"2099-08-08T13:57:36.123456Z","caller":"plog/plog.go:$plog.pLogger.debugDepth","message":"de","error":"some err","panda":2} {"level":"trace","timestamp":"2099-08-08T13:57:36.123456Z","caller":"plog/plog.go:$plog.pLogger.traceDepth","message":"t","panda":2} {"level":"trace","timestamp":"2099-08-08T13:57:36.123456Z","caller":"plog/plog.go:$plog.pLogger.traceDepth","message":"te","error":"some err","panda":2} -{"level":"all","timestamp":"2099-08-08T13:57:36.123456Z","caller":"logr@v1.4.1/logr.go:$logr.Logger.Info","message":"all","panda":2} -{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","caller":"logr@v1.4.1/logr.go:$logr.Logger.Info","message":"always","panda":2} +{"level":"all","timestamp":"2099-08-08T13:57:36.123456Z","caller":"logr@v1.4.2/logr.go:$logr.Logger.Info","message":"all","panda":2} +{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","caller":"logr@v1.4.2/logr.go:$logr.Logger.Info","message":"always","panda":2} `, }, { @@ -261,14 +261,14 @@ func TestPlog(t *testing.T) { }, want: ` {"level":"error","timestamp":"2099-08-08T13:57:36.123456Z","caller":"zapr@v1.3.0/zapr.go:$zapr.(*zapLogger).Error","message":"e","panda":2,"error":"some err"} -{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","caller":"logr@v1.4.1/logr.go:$logr.Logger.Info","message":"w","warning":true,"panda":2} -{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","caller":"logr@v1.4.1/logr.go:$logr.Logger.Info","message":"we","warning":true,"error":"some err","panda":2} -{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","caller":"logr@v1.4.1/logr.go:$logr.Logger.Info","message":"i","panda":2} -{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","caller":"logr@v1.4.1/logr.go:$logr.Logger.Info","message":"ie","error":"some err","panda":2} -{"level":"debug","timestamp":"2099-08-08T13:57:36.123456Z","caller":"logr@v1.4.1/logr.go:$logr.Logger.Info","message":"d","panda":2} -{"level":"debug","timestamp":"2099-08-08T13:57:36.123456Z","caller":"logr@v1.4.1/logr.go:$logr.Logger.Info","message":"de","error":"some err","panda":2} -{"level":"trace","timestamp":"2099-08-08T13:57:36.123456Z","caller":"logr@v1.4.1/logr.go:$logr.Logger.Info","message":"t","panda":2} -{"level":"trace","timestamp":"2099-08-08T13:57:36.123456Z","caller":"logr@v1.4.1/logr.go:$logr.Logger.Info","message":"te","error":"some err","panda":2} +{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","caller":"logr@v1.4.2/logr.go:$logr.Logger.Info","message":"w","warning":true,"panda":2} +{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","caller":"logr@v1.4.2/logr.go:$logr.Logger.Info","message":"we","warning":true,"error":"some err","panda":2} +{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","caller":"logr@v1.4.2/logr.go:$logr.Logger.Info","message":"i","panda":2} +{"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","caller":"logr@v1.4.2/logr.go:$logr.Logger.Info","message":"ie","error":"some err","panda":2} +{"level":"debug","timestamp":"2099-08-08T13:57:36.123456Z","caller":"logr@v1.4.2/logr.go:$logr.Logger.Info","message":"d","panda":2} +{"level":"debug","timestamp":"2099-08-08T13:57:36.123456Z","caller":"logr@v1.4.2/logr.go:$logr.Logger.Info","message":"de","error":"some err","panda":2} +{"level":"trace","timestamp":"2099-08-08T13:57:36.123456Z","caller":"logr@v1.4.2/logr.go:$logr.Logger.Info","message":"t","panda":2} +{"level":"trace","timestamp":"2099-08-08T13:57:36.123456Z","caller":"logr@v1.4.2/logr.go:$logr.Logger.Info","message":"te","error":"some err","panda":2} {"level":"all","timestamp":"2099-08-08T13:57:36.123456Z","caller":"zapr@v1.3.0/zapr.go:$zapr.(*zapLogger).Info","message":"all","panda":2} {"level":"info","timestamp":"2099-08-08T13:57:36.123456Z","caller":"zapr@v1.3.0/zapr.go:$zapr.(*zapLogger).Info","message":"always","panda":2}`, }, diff --git a/internal/psession/pinniped_session.go b/internal/psession/pinniped_session.go index 63f5ebe79..41fd6d330 100644 --- a/internal/psession/pinniped_session.go +++ b/internal/psession/pinniped_session.go @@ -74,6 +74,9 @@ type CustomSessionData struct { // Only used when ProviderType == "activedirectory". ActiveDirectory *ActiveDirectorySessionData `json:"activedirectory,omitempty"` + + // Only used when ProviderType == "github". + GitHub *GitHubSessionData `json:"github,omitempty"` } type ProviderType string @@ -82,6 +85,7 @@ const ( ProviderTypeOIDC ProviderType = "oidc" ProviderTypeLDAP ProviderType = "ldap" ProviderTypeActiveDirectory ProviderType = "activedirectory" + ProviderTypeGitHub ProviderType = "github" ) // OIDCSessionData is the additional data needed by Pinniped when the upstream IDP is an OIDC provider. @@ -140,6 +144,15 @@ func (s *ActiveDirectorySessionData) Clone() *ActiveDirectorySessionData { } } +type GitHubSessionData struct { + UpstreamAccessToken string `json:"upstreamAccessToken"` +} + +func (s *GitHubSessionData) Clone() *GitHubSessionData { + dataCopy := *s // this shortcut works because all fields in this type are currently strings (no pointers) + return &dataCopy +} + // NewPinnipedSession returns a new empty session. func NewPinnipedSession() *PinnipedSession { return &PinnipedSession{ diff --git a/internal/setutil/setutil.go b/internal/setutil/setutil.go new file mode 100644 index 000000000..714686c8d --- /dev/null +++ b/internal/setutil/setutil.go @@ -0,0 +1,43 @@ +// Copyright 2024 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package setutil + +import ( + "strings" + + "k8s.io/apimachinery/pkg/util/sets" + + "go.pinniped.dev/internal/sliceutil" +) + +type CaseInsensitiveSet struct { + lowercasedContents sets.Set[string] +} + +func NewCaseInsensitiveSet(items ...string) *CaseInsensitiveSet { + return &CaseInsensitiveSet{ + lowercasedContents: sets.New(sliceutil.Map(items, strings.ToLower)...), + } +} + +func (s *CaseInsensitiveSet) HasAnyIgnoringCase(items []string) bool { + if s == nil { + return false + } + return s.lowercasedContents.HasAny(sliceutil.Map(items, strings.ToLower)...) +} + +func (s *CaseInsensitiveSet) ContainsIgnoringCase(item string) bool { + if s == nil { + return false + } + return s.lowercasedContents.Has(strings.ToLower(item)) +} + +func (s *CaseInsensitiveSet) Empty() bool { + if s == nil { + return true + } + return s.lowercasedContents.Len() == 0 +} diff --git a/internal/setutil/setutil_test.go b/internal/setutil/setutil_test.go new file mode 100644 index 000000000..0fb61ede8 --- /dev/null +++ b/internal/setutil/setutil_test.go @@ -0,0 +1,34 @@ +// Copyright 2024 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package setutil + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestCaseInsensitiveSet(t *testing.T) { + var nilSet *CaseInsensitiveSet + require.True(t, nilSet.Empty()) + require.False(t, nilSet.HasAnyIgnoringCase([]string{"a", "b"})) + require.False(t, nilSet.HasAnyIgnoringCase(nil)) + require.False(t, nilSet.ContainsIgnoringCase("a")) + require.False(t, nilSet.ContainsIgnoringCase("a")) + + emptySet := NewCaseInsensitiveSet() + require.True(t, emptySet.Empty()) + require.False(t, emptySet.HasAnyIgnoringCase([]string{"a", "b"})) + require.False(t, emptySet.HasAnyIgnoringCase(nil)) + require.False(t, emptySet.ContainsIgnoringCase("a")) + require.False(t, emptySet.ContainsIgnoringCase("a")) + + set := NewCaseInsensitiveSet("A", "B", "c") + require.False(t, set.Empty()) + require.False(t, set.HasAnyIgnoringCase([]string{"x", "y"})) + require.True(t, set.HasAnyIgnoringCase([]string{"a", "x"})) + require.False(t, set.HasAnyIgnoringCase(nil)) + require.False(t, set.ContainsIgnoringCase("x")) + require.True(t, set.ContainsIgnoringCase("a")) +} diff --git a/internal/sliceutil/sliceutil.go b/internal/sliceutil/sliceutil.go new file mode 100644 index 000000000..af243ab4a --- /dev/null +++ b/internal/sliceutil/sliceutil.go @@ -0,0 +1,13 @@ +// Copyright 2024 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package sliceutil + +// Map transforms a slice from an input type I to an output type O using a transform func. +func Map[I, O any](in []I, transform func(I) O) []O { + out := make([]O, len(in)) + for i := range in { + out[i] = transform(in[i]) + } + return out +} diff --git a/internal/sliceutil/sliceutil_test.go b/internal/sliceutil/sliceutil_test.go new file mode 100644 index 000000000..389603a12 --- /dev/null +++ b/internal/sliceutil/sliceutil_test.go @@ -0,0 +1,74 @@ +// Copyright 2024 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package sliceutil + +import ( + "strings" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestMap(t *testing.T) { + type testCase[I any, O any] struct { + name string + in []I + transformFunc func(I) O + want []O + } + + stringStringTests := []testCase[string, string]{ + { + name: "downcase func", + in: []string{"Aa", "bB", "CC"}, + transformFunc: strings.ToLower, + want: []string{"aa", "bb", "cc"}, + }, + { + name: "upcase func", + in: []string{"Aa", "bB", "CC"}, + transformFunc: strings.ToUpper, + want: []string{"AA", "BB", "CC"}, + }, + { + name: "when in is nil, then out is an empty slice", + in: nil, + transformFunc: strings.ToUpper, + want: []string{}, + }, + } + for _, tt := range stringStringTests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + actual := Map(tt.in, tt.transformFunc) + require.Equal(t, tt.want, actual) + }) + } + + stringIntTests := []testCase[string, int]{ + { + name: "len func", + in: []string{"Aa", "bBb", "CCcC"}, + transformFunc: func(s string) int { + return len(s) + }, + want: []int{2, 3, 4}, + }, + { + name: "index func", + in: []string{"Aab", "bB", "CC"}, + transformFunc: func(s string) int { + return strings.Index(s, "b") + }, + want: []int{2, 0, -1}, + }, + } + for _, tt := range stringIntTests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + actual := Map(tt.in, tt.transformFunc) + require.Equal(t, tt.want, actual) + }) + } +} diff --git a/internal/supervisor/server/server.go b/internal/supervisor/server/server.go index 3dd56bafd..7d98f0a60 100644 --- a/internal/supervisor/server/server.go +++ b/internal/supervisor/server/server.go @@ -51,6 +51,7 @@ import ( "go.pinniped.dev/internal/controller/supervisorconfig" "go.pinniped.dev/internal/controller/supervisorconfig/activedirectoryupstreamwatcher" "go.pinniped.dev/internal/controller/supervisorconfig/generator" + "go.pinniped.dev/internal/controller/supervisorconfig/githubupstreamwatcher" "go.pinniped.dev/internal/controller/supervisorconfig/ldapupstreamwatcher" "go.pinniped.dev/internal/controller/supervisorconfig/oidcclientwatcher" "go.pinniped.dev/internal/controller/supervisorconfig/oidcupstreamwatcher" @@ -177,6 +178,7 @@ func prepareControllers( pinnipedInformers.IDP().V1alpha1().OIDCIdentityProviders(), pinnipedInformers.IDP().V1alpha1().LDAPIdentityProviders(), pinnipedInformers.IDP().V1alpha1().ActiveDirectoryIdentityProviders(), + pinnipedInformers.IDP().V1alpha1().GitHubIdentityProviders(), controllerlib.WithInformer, ), singletonWorker, @@ -323,6 +325,19 @@ func prepareControllers( controllerlib.WithInformer, ), singletonWorker). + WithController( + githubupstreamwatcher.New( + podInfo.Namespace, + dynamicUpstreamIDPProvider, + pinnipedClient, + pinnipedInformers.IDP().V1alpha1().GitHubIdentityProviders(), + secretInformer, + plog.New(), + controllerlib.WithInformer, + clock.RealClock{}, + tls.Dial, + ), + singletonWorker). WithController( apicerts.NewCertsManagerController( podInfo.Namespace, diff --git a/internal/testutil/oidctestutil/expected_upstream_state_param.go b/internal/testutil/oidctestutil/expected_upstream_state_param.go index f3a0bcb96..72ff6398d 100644 --- a/internal/testutil/oidctestutil/expected_upstream_state_param.go +++ b/internal/testutil/oidctestutil/expected_upstream_state_param.go @@ -8,6 +8,8 @@ import ( "github.com/gorilla/securecookie" "github.com/stretchr/testify/require" + + idpdiscoveryv1alpha1 "go.pinniped.dev/generated/latest/apis/supervisor/idpdiscovery/v1alpha1" ) // ExpectedUpstreamStateParamFormat is a separate type from the production code to ensure that the state @@ -52,8 +54,13 @@ func (b *UpstreamStateParamBuilder) WithPKCE(pkce string) *UpstreamStateParamBui return b } -func (b *UpstreamStateParamBuilder) WithUpstreamIDPType(upstreamIDPType string) *UpstreamStateParamBuilder { - b.T = upstreamIDPType +func (b *UpstreamStateParamBuilder) WithUpstreamIDPType(upstreamIDPType idpdiscoveryv1alpha1.IDPType) *UpstreamStateParamBuilder { + b.T = string(upstreamIDPType) + return b +} + +func (b *UpstreamStateParamBuilder) WithUpstreamIDPName(upstreamIDPName string) *UpstreamStateParamBuilder { + b.U = upstreamIDPName return b } diff --git a/internal/testutil/oidctestutil/testgithubprovider.go b/internal/testutil/oidctestutil/testgithubprovider.go new file mode 100644 index 000000000..aa8d1bc76 --- /dev/null +++ b/internal/testutil/oidctestutil/testgithubprovider.go @@ -0,0 +1,264 @@ +// Copyright 2020-2024 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package oidctestutil + +import ( + "context" + + "k8s.io/apimachinery/pkg/types" + + "go.pinniped.dev/generated/latest/apis/supervisor/idp/v1alpha1" + "go.pinniped.dev/internal/federationdomain/upstreamprovider" + "go.pinniped.dev/internal/idtransform" + "go.pinniped.dev/internal/setutil" +) + +// ExchangeAuthcodeArgs is used to spy on calls to +// TestUpstreamGitHubIdentityProvider.ExchangeAuthcodeFunc(). +type ExchangeAuthcodeArgs struct { + Ctx context.Context + Authcode string + RedirectURI string +} + +// GetUserArgs is used to spy on calls to +// TestUpstreamGitHubIdentityProvider.GetUserFunc(). +type GetUserArgs struct { + Ctx context.Context + AccessToken string + IDPDisplayName string +} + +type TestUpstreamGitHubIdentityProviderBuilder struct { + name string + resourceUID types.UID + clientID string + scopes []string + displayNameForFederationDomain string + transformsForFederationDomain *idtransform.TransformationPipeline + usernameAttribute v1alpha1.GitHubUsernameAttribute + groupNameAttribute v1alpha1.GitHubGroupNameAttribute + allowedOrganizations *setutil.CaseInsensitiveSet + authorizationURL string + authcodeExchangeErr error + accessToken string + getUserErr error + getUserUser *upstreamprovider.GitHubUser +} + +func (u *TestUpstreamGitHubIdentityProviderBuilder) WithName(value string) *TestUpstreamGitHubIdentityProviderBuilder { + u.name = value + return u +} + +func (u *TestUpstreamGitHubIdentityProviderBuilder) WithResourceUID(value types.UID) *TestUpstreamGitHubIdentityProviderBuilder { + u.resourceUID = value + return u +} + +func (u *TestUpstreamGitHubIdentityProviderBuilder) WithClientID(value string) *TestUpstreamGitHubIdentityProviderBuilder { + u.clientID = value + return u +} + +func (u *TestUpstreamGitHubIdentityProviderBuilder) WithScopes(value []string) *TestUpstreamGitHubIdentityProviderBuilder { + u.scopes = value + return u +} + +func (u *TestUpstreamGitHubIdentityProviderBuilder) WithDisplayNameForFederationDomain(value string) *TestUpstreamGitHubIdentityProviderBuilder { + u.displayNameForFederationDomain = value + return u +} + +func (u *TestUpstreamGitHubIdentityProviderBuilder) WithUsernameAttribute(value v1alpha1.GitHubUsernameAttribute) *TestUpstreamGitHubIdentityProviderBuilder { + u.usernameAttribute = value + return u +} + +func (u *TestUpstreamGitHubIdentityProviderBuilder) WithGroupNameAttribute(value v1alpha1.GitHubGroupNameAttribute) *TestUpstreamGitHubIdentityProviderBuilder { + u.groupNameAttribute = value + return u +} + +func (u *TestUpstreamGitHubIdentityProviderBuilder) WithAllowedOrganizations(value *setutil.CaseInsensitiveSet) *TestUpstreamGitHubIdentityProviderBuilder { + u.allowedOrganizations = value + return u +} + +func (u *TestUpstreamGitHubIdentityProviderBuilder) WithAuthorizationURL(value string) *TestUpstreamGitHubIdentityProviderBuilder { + u.authorizationURL = value + return u +} + +func (u *TestUpstreamGitHubIdentityProviderBuilder) WithAccessToken(token string) *TestUpstreamGitHubIdentityProviderBuilder { + u.accessToken = token + return u +} + +func (u *TestUpstreamGitHubIdentityProviderBuilder) WithAuthcodeExchangeError(err error) *TestUpstreamGitHubIdentityProviderBuilder { + u.authcodeExchangeErr = err + return u +} + +func (u *TestUpstreamGitHubIdentityProviderBuilder) WithUser(user *upstreamprovider.GitHubUser) *TestUpstreamGitHubIdentityProviderBuilder { + u.getUserUser = user + return u +} + +func (u *TestUpstreamGitHubIdentityProviderBuilder) WithGetUserError(err error) *TestUpstreamGitHubIdentityProviderBuilder { + u.getUserErr = err + return u +} + +func (u *TestUpstreamGitHubIdentityProviderBuilder) WithTransformsForFederationDomain(transforms *idtransform.TransformationPipeline) *TestUpstreamGitHubIdentityProviderBuilder { + u.transformsForFederationDomain = transforms + return u +} + +func (u *TestUpstreamGitHubIdentityProviderBuilder) Build() *TestUpstreamGitHubIdentityProvider { + if u.displayNameForFederationDomain == "" { + // default it to the CR name + u.displayNameForFederationDomain = u.name + } + if u.transformsForFederationDomain == nil { + // default to an empty pipeline + u.transformsForFederationDomain = idtransform.NewTransformationPipeline() + } + return &TestUpstreamGitHubIdentityProvider{ + Name: u.name, + ClientID: u.clientID, + ResourceUID: u.resourceUID, + Scopes: u.scopes, + DisplayNameForFederationDomain: u.displayNameForFederationDomain, + TransformsForFederationDomain: u.transformsForFederationDomain, + UsernameAttribute: u.usernameAttribute, + GroupNameAttribute: u.groupNameAttribute, + AllowedOrganizations: u.allowedOrganizations, + AuthorizationURL: u.authorizationURL, + GetUserFunc: func(ctx context.Context, accessToken string) (*upstreamprovider.GitHubUser, error) { + if u.getUserErr != nil { + return nil, u.getUserErr + } + return u.getUserUser, nil + }, + ExchangeAuthcodeFunc: func(ctx context.Context, authcode string) (string, error) { + if u.authcodeExchangeErr != nil { + return "", u.authcodeExchangeErr + } + return u.accessToken, nil + }, + } +} + +func NewTestUpstreamGitHubIdentityProviderBuilder() *TestUpstreamGitHubIdentityProviderBuilder { + return &TestUpstreamGitHubIdentityProviderBuilder{} +} + +type TestUpstreamGitHubIdentityProvider struct { + Name string + ClientID string + ResourceUID types.UID + Scopes []string + DisplayNameForFederationDomain string + TransformsForFederationDomain *idtransform.TransformationPipeline + UsernameAttribute v1alpha1.GitHubUsernameAttribute + GroupNameAttribute v1alpha1.GitHubGroupNameAttribute + AllowedOrganizations *setutil.CaseInsensitiveSet + AuthorizationURL string + GetUserFunc func(ctx context.Context, accessToken string) (*upstreamprovider.GitHubUser, error) + ExchangeAuthcodeFunc func(ctx context.Context, authcode string) (string, error) + + // Fields for tracking actual calls make to mock functions. + exchangeAuthcodeCallCount int + exchangeAuthcodeArgs []*ExchangeAuthcodeArgs + getUserCallCount int + getUserArgs []*GetUserArgs +} + +var _ upstreamprovider.UpstreamGithubIdentityProviderI = &TestUpstreamGitHubIdentityProvider{} + +func (u *TestUpstreamGitHubIdentityProvider) GetResourceUID() types.UID { + return u.ResourceUID +} + +func (u *TestUpstreamGitHubIdentityProvider) GetResourceName() string { + return u.Name +} + +func (u *TestUpstreamGitHubIdentityProvider) GetScopes() []string { + return u.Scopes +} + +func (u *TestUpstreamGitHubIdentityProvider) GetClientID() string { + return u.ClientID +} + +func (u *TestUpstreamGitHubIdentityProvider) GetUsernameAttribute() v1alpha1.GitHubUsernameAttribute { + return u.UsernameAttribute +} + +func (u *TestUpstreamGitHubIdentityProvider) GetGroupNameAttribute() v1alpha1.GitHubGroupNameAttribute { + return u.GroupNameAttribute +} + +func (u *TestUpstreamGitHubIdentityProvider) GetAllowedOrganizations() *setutil.CaseInsensitiveSet { + return u.AllowedOrganizations +} + +func (u *TestUpstreamGitHubIdentityProvider) GetAuthorizationURL() string { + return u.AuthorizationURL +} + +func (u *TestUpstreamGitHubIdentityProvider) ExchangeAuthcode( + ctx context.Context, + authcode string, + redirectURI string, +) (string, error) { + if u.exchangeAuthcodeArgs == nil { + u.exchangeAuthcodeArgs = make([]*ExchangeAuthcodeArgs, 0) + } + u.exchangeAuthcodeCallCount++ + u.exchangeAuthcodeArgs = append(u.exchangeAuthcodeArgs, &ExchangeAuthcodeArgs{ + Ctx: ctx, + Authcode: authcode, + RedirectURI: redirectURI, + }) + return u.ExchangeAuthcodeFunc(ctx, authcode) +} + +func (u *TestUpstreamGitHubIdentityProvider) ExchangeAuthcodeCallCount() int { + return u.exchangeAuthcodeCallCount +} + +func (u *TestUpstreamGitHubIdentityProvider) ExchangeAuthcodeArgs(call int) *ExchangeAuthcodeArgs { + if u.exchangeAuthcodeArgs == nil { + u.exchangeAuthcodeArgs = make([]*ExchangeAuthcodeArgs, 0) + } + return u.exchangeAuthcodeArgs[call] +} + +func (u *TestUpstreamGitHubIdentityProvider) GetUser(ctx context.Context, accessToken string, idpDisplayName string) (*upstreamprovider.GitHubUser, error) { + if u.getUserArgs == nil { + u.getUserArgs = make([]*GetUserArgs, 0) + } + u.getUserCallCount++ + u.getUserArgs = append(u.getUserArgs, &GetUserArgs{ + Ctx: ctx, + AccessToken: accessToken, + IDPDisplayName: idpDisplayName, + }) + return u.GetUserFunc(ctx, accessToken) +} + +func (u *TestUpstreamGitHubIdentityProvider) GetUserCallCount() int { + return u.getUserCallCount +} + +func (u *TestUpstreamGitHubIdentityProvider) GetUserArgs(call int) *GetUserArgs { + if u.getUserArgs == nil { + u.getUserArgs = make([]*GetUserArgs, 0) + } + return u.getUserArgs[call] +} diff --git a/internal/testutil/oidctestutil/testldapprovider.go b/internal/testutil/oidctestutil/testldapprovider.go index b4ef6363f..2808e9932 100644 --- a/internal/testutil/oidctestutil/testldapprovider.go +++ b/internal/testutil/oidctestutil/testldapprovider.go @@ -14,6 +14,12 @@ import ( "go.pinniped.dev/internal/idtransform" ) +type PerformLDAPRefreshArgs struct { + Ctx context.Context + StoredRefreshAttributes upstreamprovider.LDAPRefreshAttributes + IDPDisplayName string +} + func NewTestUpstreamLDAPIdentityProviderBuilder() *TestUpstreamLDAPIdentityProviderBuilder { return &TestUpstreamLDAPIdentityProviderBuilder{} } @@ -102,7 +108,7 @@ type TestUpstreamLDAPIdentityProvider struct { // Fields for tracking actual calls make to mock functions. performRefreshCallCount int - performRefreshArgs []*PerformRefreshArgs + performRefreshArgs []*PerformLDAPRefreshArgs } var _ upstreamprovider.UpstreamLDAPIdentityProviderI = &TestUpstreamLDAPIdentityProvider{} @@ -111,7 +117,7 @@ func (u *TestUpstreamLDAPIdentityProvider) GetResourceUID() types.UID { return u.ResourceUID } -func (u *TestUpstreamLDAPIdentityProvider) GetName() string { +func (u *TestUpstreamLDAPIdentityProvider) GetResourceName() string { return u.Name } @@ -123,16 +129,15 @@ func (u *TestUpstreamLDAPIdentityProvider) GetURL() *url.URL { return u.URL } -func (u *TestUpstreamLDAPIdentityProvider) PerformRefresh(ctx context.Context, storedRefreshAttributes upstreamprovider.RefreshAttributes, _idpDisplayName string) ([]string, error) { +func (u *TestUpstreamLDAPIdentityProvider) PerformRefresh(ctx context.Context, storedRefreshAttributes upstreamprovider.LDAPRefreshAttributes, idpDisplayName string) ([]string, error) { if u.performRefreshArgs == nil { - u.performRefreshArgs = make([]*PerformRefreshArgs, 0) + u.performRefreshArgs = make([]*PerformLDAPRefreshArgs, 0) } u.performRefreshCallCount++ - u.performRefreshArgs = append(u.performRefreshArgs, &PerformRefreshArgs{ - Ctx: ctx, - DN: storedRefreshAttributes.DN, - ExpectedUsername: storedRefreshAttributes.Username, - ExpectedSubject: storedRefreshAttributes.Subject, + u.performRefreshArgs = append(u.performRefreshArgs, &PerformLDAPRefreshArgs{ + Ctx: ctx, + StoredRefreshAttributes: storedRefreshAttributes, + IDPDisplayName: idpDisplayName, }) if u.PerformRefreshErr != nil { return nil, u.PerformRefreshErr @@ -144,9 +149,9 @@ func (u *TestUpstreamLDAPIdentityProvider) PerformRefreshCallCount() int { return u.performRefreshCallCount } -func (u *TestUpstreamLDAPIdentityProvider) PerformRefreshArgs(call int) *PerformRefreshArgs { +func (u *TestUpstreamLDAPIdentityProvider) PerformRefreshArgs(call int) *PerformLDAPRefreshArgs { if u.performRefreshArgs == nil { - u.performRefreshArgs = make([]*PerformRefreshArgs, 0) + u.performRefreshArgs = make([]*PerformLDAPRefreshArgs, 0) } return u.performRefreshArgs[call] } diff --git a/internal/testutil/oidctestutil/testoidcprovider.go b/internal/testutil/oidctestutil/testoidcprovider.go index 92a1d05ea..092d2081b 100644 --- a/internal/testutil/oidctestutil/testoidcprovider.go +++ b/internal/testutil/oidctestutil/testoidcprovider.go @@ -36,14 +36,11 @@ type PasswordCredentialsGrantAndValidateTokensArgs struct { Password string } -// PerformRefreshArgs is used to spy on calls to +// PerformOIDCRefreshArgs is used to spy on calls to // TestUpstreamOIDCIdentityProvider.PerformRefreshFunc(). -type PerformRefreshArgs struct { - Ctx context.Context - RefreshToken string - DN string - ExpectedUsername string - ExpectedSubject string +type PerformOIDCRefreshArgs struct { + Ctx context.Context + RefreshToken string } // RevokeTokenArgs is used to spy on calls to @@ -105,7 +102,7 @@ type TestUpstreamOIDCIdentityProvider struct { passwordCredentialsGrantAndValidateTokensCallCount int passwordCredentialsGrantAndValidateTokensArgs []*PasswordCredentialsGrantAndValidateTokensArgs performRefreshCallCount int - performRefreshArgs []*PerformRefreshArgs + performRefreshArgs []*PerformOIDCRefreshArgs revokeTokenCallCount int revokeTokenArgs []*RevokeTokenArgs validateTokenAndMergeWithUserInfoCallCount int @@ -126,7 +123,7 @@ func (u *TestUpstreamOIDCIdentityProvider) GetAdditionalClaimMappings() map[stri return u.AdditionalClaimMappings } -func (u *TestUpstreamOIDCIdentityProvider) GetName() string { +func (u *TestUpstreamOIDCIdentityProvider) GetResourceName() string { return u.Name } @@ -217,10 +214,10 @@ func (u *TestUpstreamOIDCIdentityProvider) PasswordCredentialsGrantAndValidateTo func (u *TestUpstreamOIDCIdentityProvider) PerformRefresh(ctx context.Context, refreshToken string) (*oauth2.Token, error) { if u.performRefreshArgs == nil { - u.performRefreshArgs = make([]*PerformRefreshArgs, 0) + u.performRefreshArgs = make([]*PerformOIDCRefreshArgs, 0) } u.performRefreshCallCount++ - u.performRefreshArgs = append(u.performRefreshArgs, &PerformRefreshArgs{ + u.performRefreshArgs = append(u.performRefreshArgs, &PerformOIDCRefreshArgs{ Ctx: ctx, RefreshToken: refreshToken, }) @@ -244,9 +241,9 @@ func (u *TestUpstreamOIDCIdentityProvider) PerformRefreshCallCount() int { return u.performRefreshCallCount } -func (u *TestUpstreamOIDCIdentityProvider) PerformRefreshArgs(call int) *PerformRefreshArgs { +func (u *TestUpstreamOIDCIdentityProvider) PerformRefreshArgs(call int) *PerformOIDCRefreshArgs { if u.performRefreshArgs == nil { - u.performRefreshArgs = make([]*PerformRefreshArgs, 0) + u.performRefreshArgs = make([]*PerformOIDCRefreshArgs, 0) } return u.performRefreshArgs[call] } diff --git a/internal/testutil/testidplister/testidplister.go b/internal/testutil/testidplister/testidplister.go index 6212ce3db..bbb2b340a 100644 --- a/internal/testutil/testidplister/testidplister.go +++ b/internal/testutil/testidplister/testidplister.go @@ -12,6 +12,7 @@ import ( "go.pinniped.dev/internal/federationdomain/dynamicupstreamprovider" "go.pinniped.dev/internal/federationdomain/resolvedprovider" + "go.pinniped.dev/internal/federationdomain/resolvedprovider/resolvedgithub" "go.pinniped.dev/internal/federationdomain/resolvedprovider/resolvedldap" "go.pinniped.dev/internal/federationdomain/resolvedprovider/resolvedoidc" "go.pinniped.dev/internal/federationdomain/upstreamprovider" @@ -25,6 +26,7 @@ type TestFederationDomainIdentityProvidersListerFinder struct { upstreamOIDCIdentityProviders []*oidctestutil.TestUpstreamOIDCIdentityProvider upstreamLDAPIdentityProviders []*oidctestutil.TestUpstreamLDAPIdentityProvider upstreamActiveDirectoryIdentityProviders []*oidctestutil.TestUpstreamLDAPIdentityProvider + upstreamGitHubIdentityProviders []*oidctestutil.TestUpstreamGitHubIdentityProvider defaultIDPDisplayName string } @@ -38,7 +40,7 @@ func (t *TestFederationDomainIdentityProvidersListerFinder) IDPCount() int { func (t *TestFederationDomainIdentityProvidersListerFinder) GetIdentityProviders() []resolvedprovider.FederationDomainResolvedIdentityProvider { fdIDPs := make([]resolvedprovider.FederationDomainResolvedIdentityProvider, - len(t.upstreamOIDCIdentityProviders)+len(t.upstreamLDAPIdentityProviders)+len(t.upstreamActiveDirectoryIdentityProviders)) + len(t.upstreamOIDCIdentityProviders)+len(t.upstreamLDAPIdentityProviders)+len(t.upstreamActiveDirectoryIdentityProviders)+len(t.upstreamGitHubIdentityProviders)) i := 0 for _, testIDP := range t.upstreamOIDCIdentityProviders { fdIDP := &resolvedoidc.FederationDomainResolvedOIDCIdentityProvider{ @@ -70,6 +72,16 @@ func (t *TestFederationDomainIdentityProvidersListerFinder) GetIdentityProviders fdIDPs[i] = fdIDP i++ } + for _, testIDP := range t.upstreamGitHubIdentityProviders { + fdIDP := &resolvedgithub.FederationDomainResolvedGitHubIdentityProvider{ + DisplayName: testIDP.DisplayNameForFederationDomain, + Provider: testIDP, + SessionProviderType: psession.ProviderTypeGitHub, + Transforms: testIDP.TransformsForFederationDomain, + } + fdIDPs[i] = fdIDP + i++ + } return fdIDPs } @@ -111,6 +123,16 @@ func (t *TestFederationDomainIdentityProvidersListerFinder) FindUpstreamIDPByDis }, nil } } + for _, testIDP := range t.upstreamGitHubIdentityProviders { + if upstreamIDPDisplayName == testIDP.DisplayNameForFederationDomain { + return &resolvedgithub.FederationDomainResolvedGitHubIdentityProvider{ + DisplayName: testIDP.DisplayNameForFederationDomain, + Provider: testIDP, + SessionProviderType: psession.ProviderTypeGitHub, + Transforms: testIDP.TransformsForFederationDomain, + }, nil + } + } return nil, fmt.Errorf("did not find IDP with name %q", upstreamIDPDisplayName) } @@ -126,12 +148,17 @@ func (t *TestFederationDomainIdentityProvidersListerFinder) SetActiveDirectoryId t.upstreamActiveDirectoryIdentityProviders = providers } +func (t *TestFederationDomainIdentityProvidersListerFinder) SetGitHubIdentityProviders(providers []*oidctestutil.TestUpstreamGitHubIdentityProvider) { + t.upstreamGitHubIdentityProviders = providers +} + // UpstreamIDPListerBuilder can be used to build either a dynamicupstreamprovider.DynamicUpstreamIDPProvider // or a FederationDomainIdentityProvidersListerFinderI for testing. type UpstreamIDPListerBuilder struct { upstreamOIDCIdentityProviders []*oidctestutil.TestUpstreamOIDCIdentityProvider upstreamLDAPIdentityProviders []*oidctestutil.TestUpstreamLDAPIdentityProvider upstreamActiveDirectoryIdentityProviders []*oidctestutil.TestUpstreamLDAPIdentityProvider + upstreamGitHubIdentityProviders []*oidctestutil.TestUpstreamGitHubIdentityProvider defaultIDPDisplayName string } @@ -150,6 +177,11 @@ func (b *UpstreamIDPListerBuilder) WithActiveDirectory(upstreamActiveDirectoryId return b } +func (b *UpstreamIDPListerBuilder) WithGitHub(upstreamGithubIdentityProviders ...*oidctestutil.TestUpstreamGitHubIdentityProvider) *UpstreamIDPListerBuilder { + b.upstreamGitHubIdentityProviders = append(b.upstreamGitHubIdentityProviders, upstreamGithubIdentityProviders...) + return b +} + func (b *UpstreamIDPListerBuilder) WithDefaultIDPDisplayName(defaultIDPDisplayName string) *UpstreamIDPListerBuilder { b.defaultIDPDisplayName = defaultIDPDisplayName return b @@ -160,6 +192,7 @@ func (b *UpstreamIDPListerBuilder) BuildFederationDomainIdentityProvidersListerF upstreamOIDCIdentityProviders: b.upstreamOIDCIdentityProviders, upstreamLDAPIdentityProviders: b.upstreamLDAPIdentityProviders, upstreamActiveDirectoryIdentityProviders: b.upstreamActiveDirectoryIdentityProviders, + upstreamGitHubIdentityProviders: b.upstreamGitHubIdentityProviders, defaultIDPDisplayName: b.defaultIDPDisplayName, } } @@ -185,6 +218,12 @@ func (b *UpstreamIDPListerBuilder) BuildDynamicUpstreamIDPProvider() dynamicupst } idpProvider.SetActiveDirectoryIdentityProviders(adUpstreams) + githubUpstreams := make([]upstreamprovider.UpstreamGithubIdentityProviderI, len(b.upstreamGitHubIdentityProviders)) + for i := range b.upstreamGitHubIdentityProviders { + githubUpstreams[i] = upstreamprovider.UpstreamGithubIdentityProviderI(b.upstreamGitHubIdentityProviders[i]) + } + idpProvider.SetGitHubIdentityProviders(githubUpstreams) + return idpProvider } @@ -225,7 +264,7 @@ func (b *UpstreamIDPListerBuilder) RequireExactlyZeroCallsToPasswordCredentialsG ) } -func (b *UpstreamIDPListerBuilder) RequireExactlyOneCallToExchangeAuthcodeAndValidateTokens( +func (b *UpstreamIDPListerBuilder) RequireExactlyOneOIDCAuthcodeExchange( t *testing.T, expectedPerformedByUpstreamName string, expectedArgs *oidctestutil.ExchangeAuthcodeAndValidateTokenArgs, @@ -233,79 +272,170 @@ func (b *UpstreamIDPListerBuilder) RequireExactlyOneCallToExchangeAuthcodeAndVal t.Helper() var actualArgs *oidctestutil.ExchangeAuthcodeAndValidateTokenArgs var actualNameOfUpstreamWhichMadeCall string - actualCallCountAcrossAllOIDCUpstreams := 0 - for _, upstreamOIDC := range b.upstreamOIDCIdentityProviders { - callCountOnThisUpstream := upstreamOIDC.ExchangeAuthcodeAndValidateTokensCallCount() - actualCallCountAcrossAllOIDCUpstreams += callCountOnThisUpstream + actualCallCount := 0 + for _, upstream := range b.upstreamOIDCIdentityProviders { + callCountOnThisUpstream := upstream.ExchangeAuthcodeAndValidateTokensCallCount() + actualCallCount += callCountOnThisUpstream if callCountOnThisUpstream == 1 { - actualNameOfUpstreamWhichMadeCall = upstreamOIDC.Name - actualArgs = upstreamOIDC.ExchangeAuthcodeAndValidateTokensArgs(0) + actualNameOfUpstreamWhichMadeCall = upstream.Name + actualArgs = upstream.ExchangeAuthcodeAndValidateTokensArgs(0) } } - require.Equal(t, 1, actualCallCountAcrossAllOIDCUpstreams, - "should have been exactly one call to ExchangeAuthcodeAndValidateTokens() by all OIDC upstreams", + require.Equal(t, 1, actualCallCount, + "expected exactly one call to OIDC ExchangeAuthcodeAndValidateTokens()", ) require.Equal(t, expectedPerformedByUpstreamName, actualNameOfUpstreamWhichMadeCall, - "ExchangeAuthcodeAndValidateTokens() was called on the wrong OIDC upstream", + "OIDC ExchangeAuthcodeAndValidateTokens() was called on the wrong upstream name", ) require.Equal(t, expectedArgs, actualArgs) } -func (b *UpstreamIDPListerBuilder) RequireExactlyZeroCallsToExchangeAuthcodeAndValidateTokens(t *testing.T) { - t.Helper() - actualCallCountAcrossAllOIDCUpstreams := 0 - for _, upstreamOIDC := range b.upstreamOIDCIdentityProviders { - actualCallCountAcrossAllOIDCUpstreams += upstreamOIDC.ExchangeAuthcodeAndValidateTokensCallCount() - } - require.Equal(t, 0, actualCallCountAcrossAllOIDCUpstreams, - "expected exactly zero calls to ExchangeAuthcodeAndValidateTokens()", - ) -} - -func (b *UpstreamIDPListerBuilder) RequireExactlyOneCallToPerformRefresh( +func (b *UpstreamIDPListerBuilder) RequireExactlyOneGitHubAuthcodeExchange( t *testing.T, expectedPerformedByUpstreamName string, - expectedArgs *oidctestutil.PerformRefreshArgs, + expectedArgs *oidctestutil.ExchangeAuthcodeArgs, ) { t.Helper() - var actualArgs *oidctestutil.PerformRefreshArgs + var actualArgs *oidctestutil.ExchangeAuthcodeArgs var actualNameOfUpstreamWhichMadeCall string - actualCallCountAcrossAllUpstreams := 0 - for _, upstreamOIDC := range b.upstreamOIDCIdentityProviders { - callCountOnThisUpstream := upstreamOIDC.PerformRefreshCallCount() - actualCallCountAcrossAllUpstreams += callCountOnThisUpstream + actualCallCount := 0 + for _, upstream := range b.upstreamGitHubIdentityProviders { + callCountOnThisUpstream := upstream.ExchangeAuthcodeCallCount() + actualCallCount += callCountOnThisUpstream if callCountOnThisUpstream == 1 { - actualNameOfUpstreamWhichMadeCall = upstreamOIDC.Name - actualArgs = upstreamOIDC.PerformRefreshArgs(0) + actualNameOfUpstreamWhichMadeCall = upstream.Name + actualArgs = upstream.ExchangeAuthcodeArgs(0) } } - for _, upstreamLDAP := range b.upstreamLDAPIdentityProviders { - callCountOnThisUpstream := upstreamLDAP.PerformRefreshCallCount() - actualCallCountAcrossAllUpstreams += callCountOnThisUpstream - if callCountOnThisUpstream == 1 { - actualNameOfUpstreamWhichMadeCall = upstreamLDAP.Name - actualArgs = upstreamLDAP.PerformRefreshArgs(0) - } - } - for _, upstreamAD := range b.upstreamActiveDirectoryIdentityProviders { - callCountOnThisUpstream := upstreamAD.PerformRefreshCallCount() - actualCallCountAcrossAllUpstreams += callCountOnThisUpstream - if callCountOnThisUpstream == 1 { - actualNameOfUpstreamWhichMadeCall = upstreamAD.Name - actualArgs = upstreamAD.PerformRefreshArgs(0) - } - } - require.Equal(t, 1, actualCallCountAcrossAllUpstreams, - "should have been exactly one call to PerformRefresh() by all upstreams", + require.Equal(t, 1, actualCallCount, + "expected exactly one call to GitHub ExchangeAuthcode()", ) require.Equal(t, expectedPerformedByUpstreamName, actualNameOfUpstreamWhichMadeCall, - "PerformRefresh() was called on the wrong upstream", + "GitHub ExchangeAuthcode() was called on the wrong upstream name", ) require.Equal(t, expectedArgs, actualArgs) } -func (b *UpstreamIDPListerBuilder) RequireExactlyZeroCallsToPerformRefresh(t *testing.T) { +func (b *UpstreamIDPListerBuilder) RequireExactlyZeroAuthcodeExchanges(t *testing.T) { t.Helper() + actualCallCount := 0 + for _, upstreamOIDC := range b.upstreamOIDCIdentityProviders { + actualCallCount += upstreamOIDC.ExchangeAuthcodeAndValidateTokensCallCount() + } + for _, upstreamGitHub := range b.upstreamGitHubIdentityProviders { + actualCallCount += upstreamGitHub.ExchangeAuthcodeCallCount() + } + + require.Equal(t, 0, actualCallCount, + "expected exactly zero calls to OIDC ExchangeAuthcodeAndValidateTokens() or GitHub ExchangeAuthcode()", + ) +} + +func (b *UpstreamIDPListerBuilder) RequireExactlyOneCallToOIDCPerformRefresh( + t *testing.T, + expectedPerformedByUpstreamName string, + expectedArgs *oidctestutil.PerformOIDCRefreshArgs, +) { + t.Helper() + var actualArgs *oidctestutil.PerformOIDCRefreshArgs + var actualNameOfUpstreamWhichMadeCall string + for _, upstream := range b.upstreamOIDCIdentityProviders { + callCountOnThisUpstream := upstream.PerformRefreshCallCount() + if callCountOnThisUpstream == 1 { + actualNameOfUpstreamWhichMadeCall = upstream.Name + actualArgs = upstream.PerformRefreshArgs(0) + } + } + require.Equal(t, 1, b.CountAllCallsToAnyUpstreamRefresh(), + "should have been exactly one call to upstream refresh by all upstreams", + ) + require.Equal(t, expectedPerformedByUpstreamName, actualNameOfUpstreamWhichMadeCall, + "upstream refresh was called on the wrong upstream", + ) + require.Equal(t, expectedArgs, actualArgs) +} + +func (b *UpstreamIDPListerBuilder) RequireExactlyOneCallToActiveDirectoryPerformRefresh( + t *testing.T, + expectedPerformedByUpstreamName string, + expectedArgs *oidctestutil.PerformLDAPRefreshArgs, +) { + t.Helper() + var actualArgs *oidctestutil.PerformLDAPRefreshArgs + var actualNameOfUpstreamWhichMadeCall string + for _, upstream := range b.upstreamActiveDirectoryIdentityProviders { + callCountOnThisUpstream := upstream.PerformRefreshCallCount() + if callCountOnThisUpstream == 1 { + actualNameOfUpstreamWhichMadeCall = upstream.Name + actualArgs = upstream.PerformRefreshArgs(0) + } + } + require.Equal(t, 1, b.CountAllCallsToAnyUpstreamRefresh(), + "should have been exactly one call to upstream refresh by all upstreams", + ) + require.Equal(t, expectedPerformedByUpstreamName, actualNameOfUpstreamWhichMadeCall, + "upstream refresh was called on the wrong upstream", + ) + require.Equal(t, expectedArgs, actualArgs) +} + +func (b *UpstreamIDPListerBuilder) RequireExactlyOneCallToLDAPPerformRefresh( + t *testing.T, + expectedPerformedByUpstreamName string, + expectedArgs *oidctestutil.PerformLDAPRefreshArgs, +) { + t.Helper() + var actualArgs *oidctestutil.PerformLDAPRefreshArgs + var actualNameOfUpstreamWhichMadeCall string + for _, upstream := range b.upstreamLDAPIdentityProviders { + callCountOnThisUpstream := upstream.PerformRefreshCallCount() + if callCountOnThisUpstream == 1 { + actualNameOfUpstreamWhichMadeCall = upstream.Name + actualArgs = upstream.PerformRefreshArgs(0) + } + } + require.Equal(t, 1, b.CountAllCallsToAnyUpstreamRefresh(), + "should have been exactly one call to upstream refresh by all upstreams", + ) + require.Equal(t, expectedPerformedByUpstreamName, actualNameOfUpstreamWhichMadeCall, + "upstream refresh was called on the wrong upstream", + ) + require.Equal(t, expectedArgs, actualArgs) +} + +func (b *UpstreamIDPListerBuilder) RequireExactlyOneCallToGithubGetUser( + t *testing.T, + expectedPerformedByUpstreamName string, + expectedArgs *oidctestutil.GetUserArgs, +) { + t.Helper() + var actualArgs *oidctestutil.GetUserArgs + var actualNameOfUpstreamWhichMadeCall string + for _, upstream := range b.upstreamGitHubIdentityProviders { + // GitHub calls GetUser during both the original authcode exchange and the refresh. + callCountOnThisUpstream := upstream.GetUserCallCount() + if callCountOnThisUpstream == 1 { + actualNameOfUpstreamWhichMadeCall = upstream.Name + actualArgs = upstream.GetUserArgs(0) + } + } + require.Equal(t, 1, b.CountAllCallsToAnyUpstreamRefresh(), + "should have been exactly one call to upstream refresh by all upstreams", + ) + require.Equal(t, expectedPerformedByUpstreamName, actualNameOfUpstreamWhichMadeCall, + "upstream refresh was called on the wrong upstream", + ) + require.Equal(t, expectedArgs, actualArgs) +} + +func (b *UpstreamIDPListerBuilder) RequireExactlyZeroCallsToAnyUpstreamRefresh(t *testing.T) { + t.Helper() + require.Equal(t, 0, b.CountAllCallsToAnyUpstreamRefresh(), + "expected exactly zero calls to any upstream refresh mocks", + ) +} + +func (b *UpstreamIDPListerBuilder) CountAllCallsToAnyUpstreamRefresh() int { actualCallCountAcrossAllUpstreams := 0 for _, upstreamOIDC := range b.upstreamOIDCIdentityProviders { actualCallCountAcrossAllUpstreams += upstreamOIDC.PerformRefreshCallCount() @@ -316,10 +446,10 @@ func (b *UpstreamIDPListerBuilder) RequireExactlyZeroCallsToPerformRefresh(t *te for _, upstreamActiveDirectory := range b.upstreamActiveDirectoryIdentityProviders { actualCallCountAcrossAllUpstreams += upstreamActiveDirectory.PerformRefreshCallCount() } - - require.Equal(t, 0, actualCallCountAcrossAllUpstreams, - "expected exactly zero calls to PerformRefresh()", - ) + for _, upstreamGithub := range b.upstreamGitHubIdentityProviders { + actualCallCountAcrossAllUpstreams += upstreamGithub.GetUserCallCount() + } + return actualCallCountAcrossAllUpstreams } func (b *UpstreamIDPListerBuilder) RequireExactlyOneCallToValidateToken( diff --git a/internal/testutil/tlsserver/tlsserver.go b/internal/testutil/tlsserver/tlsserver.go index a6b3f3457..55696b0a6 100644 --- a/internal/testutil/tlsserver/tlsserver.go +++ b/internal/testutil/tlsserver/tlsserver.go @@ -43,7 +43,6 @@ func TestServerIPv6(t *testing.T, handler http.Handler, f func(*httptest.Server) Listener: listener, Config: &http.Server{Handler: handler}, //nolint:gosec //ReadHeaderTimeout is not needed for a localhost listener } - return testServer(t, server, f) } diff --git a/internal/testutil/totp/totp.go b/internal/testutil/totp/totp.go new file mode 100644 index 000000000..7e2c153cf --- /dev/null +++ b/internal/testutil/totp/totp.go @@ -0,0 +1,88 @@ +// Copyright 2024 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package totp + +import ( + "crypto/hmac" + "crypto/sha1" //nolint:gosec // This is an implementation of an RFC that used SHA-1 + "encoding/base32" + "encoding/binary" + "fmt" + "math" + "strings" + "testing" + "time" + + "github.com/stretchr/testify/require" +) + +// This code is borrowed from +// https://github.com/yitsushi/totp-cli/blob/b26f5673ae2e5cc682fc1f5ed771cb08a6403283/internal/security/otp.go +// and +// https://github.com/yitsushi/totp-cli/blob/b26f5673ae2e5cc682fc1f5ed771cb08a6403283/internal/security/error.go +// which is MIT licensed. The MIT license allows copying. +// We are choosing to copying rather than take on a whole new project dependency just for a small test helper. + +const ( + mask1 = 0xf + mask2 = 0x7f + mask3 = 0xff + timeSplitInSeconds = 30 + shift24 = 24 + shift16 = 16 + shift8 = 8 + sumByteLength = 8 +) + +// OTPError is an error describing an error during generation. +type OTPError struct { + Message string +} + +func (e OTPError) Error() string { + return "otp error: " + e.Message +} + +// GenerateOTPCode generates a 6 digit TOTP from the secret Token. +func GenerateOTPCode(t *testing.T, token string, when time.Time) (string, int64) { + t.Helper() + + require.NotEmpty(t, token) + + timer := uint64(math.Floor(float64(when.Unix()) / float64(timeSplitInSeconds))) + remainingTime := timeSplitInSeconds - when.Unix()%timeSplitInSeconds + + // Remove spaces, some providers are giving us in a readable format, + // so they add spaces in there. If it's not removed while pasting in, + // remove it now. + token = strings.ReplaceAll(token, " ", "") + + // It should be uppercase always + token = strings.ToUpper(token) + + secretBytes, err := base32.StdEncoding.WithPadding(base32.NoPadding).DecodeString(token) + require.NoError(t, err) + + length := 6 + + buf := make([]byte, sumByteLength) + mac := hmac.New(sha1.New, secretBytes) + + binary.BigEndian.PutUint64(buf, timer) + _, _ = mac.Write(buf) + sum := mac.Sum(nil) + + // http://tools.ietf.org/html/rfc4226#section-5.4 + offset := sum[len(sum)-1] & mask1 + value := int64(((int(sum[offset]) & mask2) << shift24) | + ((int(sum[offset+1] & mask3)) << shift16) | + ((int(sum[offset+2] & mask3)) << shift8) | + (int(sum[offset+3]) & mask3)) + + modulo := int32(value % int64(math.Pow10(length))) + + format := fmt.Sprintf("%%0%dd", length) + + return fmt.Sprintf(format, modulo), remainingTime +} diff --git a/internal/testutil/totp/totp_test.go b/internal/testutil/totp/totp_test.go new file mode 100644 index 000000000..6a7fcdd75 --- /dev/null +++ b/internal/testutil/totp/totp_test.go @@ -0,0 +1,47 @@ +// Copyright 2024 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package totp + +import ( + "testing" + "time" + + "github.com/stretchr/testify/require" +) + +func TestGenerateOTPCode(t *testing.T) { + tests := []struct { + name string + token string + when time.Time + wantCode string + wantRemainingLifetimeSeconds int64 + }{ + { + name: "Use a token from online example", + token: "JBSWY3DPEHPK3PXP", // https://github.com/pquerna/otp/blob/3357de7c04813a328d6a1e4a514854213e0f8ce8/totp/totp.go#L180 + when: time.Unix(1715205169, 0), + wantCode: "780919", + wantRemainingLifetimeSeconds: 11, + }, + { + name: "Use a token that was randomly generated", + token: "EDAYKXL3TEYZNQ3O4N5KPSUAQQLZYUJG", + when: time.Unix(1715225917, 0), + wantCode: "920615", + wantRemainingLifetimeSeconds: 23, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + t.Parallel() + + actualCode, actualRemainingLifetimeSeconds := GenerateOTPCode(t, test.token, test.when) + + require.Equal(t, test.wantCode, actualCode) + require.Equal(t, test.wantRemainingLifetimeSeconds, actualRemainingLifetimeSeconds) + }) + } +} diff --git a/internal/upstreamgithub/upstreamgithub.go b/internal/upstreamgithub/upstreamgithub.go new file mode 100644 index 000000000..6660787a1 --- /dev/null +++ b/internal/upstreamgithub/upstreamgithub.go @@ -0,0 +1,192 @@ +// Copyright 2020-2024 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +// Package upstreamgithub implements an abstraction of upstream GitHub provider interactions. +package upstreamgithub + +import ( + "context" + "fmt" + "net/http" + + coreosoidc "github.com/coreos/go-oidc/v3/oidc" + "golang.org/x/oauth2" + "k8s.io/apimachinery/pkg/types" + + supervisoridpv1alpha1 "go.pinniped.dev/generated/latest/apis/supervisor/idp/v1alpha1" + "go.pinniped.dev/internal/federationdomain/downstreamsubject" + "go.pinniped.dev/internal/federationdomain/upstreamprovider" + "go.pinniped.dev/internal/githubclient" + "go.pinniped.dev/internal/plog" + "go.pinniped.dev/internal/setutil" +) + +// ProviderConfig holds the active configuration of an upstream GitHub provider. +type ProviderConfig struct { + Name string + ResourceUID types.UID + + // APIBaseURL is the url of the GitHub API, not including the path to a specific API endpoint. + // According to the GitHub docs, it should be either https://api.github.com/ for cloud + // or https://HOSTNAME/api/v3/ for Enterprise Server. + APIBaseURL string + + UsernameAttribute supervisoridpv1alpha1.GitHubUsernameAttribute + GroupNameAttribute supervisoridpv1alpha1.GitHubGroupNameAttribute + + // AllowedOrganizations, when empty, means to allow users from all orgs. + AllowedOrganizations *setutil.CaseInsensitiveSet + + // HttpClient is a client that can be used to call the GitHub APIs and token endpoint. + // This client should be configured with the user-provided CA bundle and a timeout. + HttpClient *http.Client + + // OAuth2Config contains ClientID, ClientSecret, Scopes, and Endpoint (which contains auth and token endpoint URLs, + // and auth style for the token endpoint). + // OAuth2Config will not be used to compute the authorize URL because the redirect back to the Supervisor's + // callback must be different per FederationDomain. It holds data that may be useful when calculating the + // authorize URL, so that data is exposed by interface methods. However, it can be used to call the token endpoint, + // for which there is no RedirectURL needed. + OAuth2Config *oauth2.Config +} + +type Provider struct { + c ProviderConfig + buildGitHubClient func(httpClient *http.Client, apiBaseURL, token string) (githubclient.GitHubInterface, error) +} + +var _ upstreamprovider.UpstreamGithubIdentityProviderI = &Provider{} + +// New creates a Provider. The config is not a pointer to ensure that a copy of the config is created, +// making the resulting Provider use an effectively read-only configuration. +func New(config ProviderConfig) *Provider { + return &Provider{ + c: config, + buildGitHubClient: githubclient.NewGitHubClient, + } +} + +func (p *Provider) GetResourceName() string { + return p.c.Name +} + +func (p *Provider) GetResourceUID() types.UID { + return p.c.ResourceUID +} + +func (p *Provider) GetClientID() string { + return p.c.OAuth2Config.ClientID +} + +func (p *Provider) GetScopes() []string { + return p.c.OAuth2Config.Scopes +} + +func (p *Provider) GetUsernameAttribute() supervisoridpv1alpha1.GitHubUsernameAttribute { + return p.c.UsernameAttribute +} + +func (p *Provider) GetGroupNameAttribute() supervisoridpv1alpha1.GitHubGroupNameAttribute { + return p.c.GroupNameAttribute +} + +func (p *Provider) GetAllowedOrganizations() *setutil.CaseInsensitiveSet { + return p.c.AllowedOrganizations +} + +func (p *Provider) GetAuthorizationURL() string { + return p.c.OAuth2Config.Endpoint.AuthURL +} + +func (p *Provider) ExchangeAuthcode(ctx context.Context, authcode string, redirectURI string) (string, error) { + tok, err := p.c.OAuth2Config.Exchange( + coreosoidc.ClientContext(ctx, p.c.HttpClient), + authcode, + oauth2.SetAuthURLParam("redirect_uri", redirectURI), + ) + if err != nil { + return "", fmt.Errorf("error exchanging authorization code using GitHub API: %w", err) + } + return tok.AccessToken, nil +} + +// GetUser will use the provided configuration to make HTTPS calls to the GitHub API to get the identity of the +// authenticated user and to discover their org and team memberships. +// If the user's information meets the AllowedOrganization criteria specified on the GitHubIdentityProvider, +// they will be allowed to log in. +// Note that errors from the githubclient package already have helpful error prefixes, so there is no need for additional prefixes here. +func (p *Provider) GetUser(ctx context.Context, accessToken string, idpDisplayName string) (*upstreamprovider.GitHubUser, error) { + githubClient, err := p.buildGitHubClient(p.c.HttpClient, p.c.APIBaseURL, accessToken) + if err != nil { + return nil, err + } + + githubUser := upstreamprovider.GitHubUser{} + + userInfo, err := githubClient.GetUserInfo(ctx) + if err != nil { + return nil, err + } + + githubUser.DownstreamSubject = downstreamsubject.GitHub(p.c.APIBaseURL, idpDisplayName, userInfo.Login, userInfo.ID) + + switch p.c.UsernameAttribute { + case supervisoridpv1alpha1.GitHubUsernameLoginAndID: + githubUser.Username = fmt.Sprintf("%s:%s", userInfo.Login, userInfo.ID) + case supervisoridpv1alpha1.GitHubUsernameLogin: + githubUser.Username = userInfo.Login + case supervisoridpv1alpha1.GitHubUsernameID: + githubUser.Username = userInfo.ID + default: + return nil, fmt.Errorf("bad configuration: unknown GitHub username attribute: %s", p.c.UsernameAttribute) + } + + orgMembership, err := githubClient.GetOrgMembership(ctx) + if err != nil { + return nil, err + } + + if !p.c.AllowedOrganizations.Empty() && !p.c.AllowedOrganizations.HasAnyIgnoringCase(orgMembership) { + plog.Warning("user is not allowed to log in due to organization membership policy", // do not log username to avoid PII + "userBelongsToOrganizations", orgMembership, + "configuredAllowedOrganizations", p.c.AllowedOrganizations, + "identityProviderDisplayName", idpDisplayName, + "identityProviderResourceName", p.GetResourceName()) + plog.Trace("user is not allowed to log in due to organization membership policy", // okay to log PII at trace level + "githubLogin", userInfo.Login, + "githubID", userInfo.ID, + "calculatedUsername", githubUser.Username, + "userBelongsToOrganizations", orgMembership, + "configuredAllowedOrganizations", p.c.AllowedOrganizations, + "identityProviderDisplayName", idpDisplayName, + "identityProviderResourceName", p.GetResourceName()) + return nil, upstreamprovider.NewGitHubLoginDeniedError("user is not allowed to log in due to organization membership policy") + } + + teamMembership, err := githubClient.GetTeamMembership(ctx, p.c.AllowedOrganizations) + if err != nil { + return nil, err + } + + for _, team := range teamMembership { + downstreamGroup := "" + + switch p.c.GroupNameAttribute { + case supervisoridpv1alpha1.GitHubUseTeamNameForGroupName: + downstreamGroup = fmt.Sprintf("%s/%s", team.Org, team.Name) + case supervisoridpv1alpha1.GitHubUseTeamSlugForGroupName: + downstreamGroup = fmt.Sprintf("%s/%s", team.Org, team.Slug) + default: + return nil, fmt.Errorf("bad configuration: unknown GitHub group name attribute: %s", p.c.GroupNameAttribute) + } + + githubUser.Groups = append(githubUser.Groups, downstreamGroup) + } + + return &githubUser, nil +} + +// GetConfig returns the config. This is not part of the UpstreamGithubIdentityProviderI interface and is just for testing. +func (p *Provider) GetConfig() ProviderConfig { + return p.c +} diff --git a/internal/upstreamgithub/upstreamgithub_test.go b/internal/upstreamgithub/upstreamgithub_test.go new file mode 100644 index 000000000..f2dd1263d --- /dev/null +++ b/internal/upstreamgithub/upstreamgithub_test.go @@ -0,0 +1,511 @@ +// Copyright 2020-2024 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package upstreamgithub + +import ( + "context" + "crypto/tls" + "errors" + "fmt" + "net/http" + "testing" + "time" + + "github.com/stretchr/testify/require" + "go.uber.org/mock/gomock" + "golang.org/x/oauth2" + "k8s.io/apimachinery/pkg/types" + "k8s.io/apimachinery/pkg/util/rand" + "k8s.io/client-go/util/cert" + + supervisoridpv1alpha1 "go.pinniped.dev/generated/latest/apis/supervisor/idp/v1alpha1" + "go.pinniped.dev/internal/federationdomain/upstreamprovider" + "go.pinniped.dev/internal/githubclient" + "go.pinniped.dev/internal/mocks/mockgithubclient" + "go.pinniped.dev/internal/setutil" + "go.pinniped.dev/internal/testutil/tlsserver" +) + +func TestGitHubProvider(t *testing.T) { + subject := New(ProviderConfig{ + Name: "foo", + ResourceUID: "resource-uid-12345", + APIBaseURL: "https://fake-base-url", + UsernameAttribute: "fake-username-attribute", + GroupNameAttribute: "fake-group-name-attribute", + OAuth2Config: &oauth2.Config{ + ClientID: "fake-client-id", + ClientSecret: "fake-client-secret", + Scopes: []string{"scope1", "scope2"}, + Endpoint: oauth2.Endpoint{ + AuthURL: "https://fake-authorization-url", + DeviceAuthURL: "", + TokenURL: "https://fake-token-url", + AuthStyle: oauth2.AuthStyleInParams, + }, + }, + AllowedOrganizations: setutil.NewCaseInsensitiveSet("fake-org", "fake-org2"), + HttpClient: &http.Client{ + Timeout: 1234509, + }, + }) + + require.Equal(t, ProviderConfig{ + Name: "foo", + ResourceUID: "resource-uid-12345", + APIBaseURL: "https://fake-base-url", + UsernameAttribute: "fake-username-attribute", + GroupNameAttribute: "fake-group-name-attribute", + OAuth2Config: &oauth2.Config{ + ClientID: "fake-client-id", + ClientSecret: "fake-client-secret", + Scopes: []string{"scope1", "scope2"}, + Endpoint: oauth2.Endpoint{ + AuthURL: "https://fake-authorization-url", + DeviceAuthURL: "", + TokenURL: "https://fake-token-url", + AuthStyle: oauth2.AuthStyleInParams, + }, + }, + AllowedOrganizations: setutil.NewCaseInsensitiveSet("fake-org", "fake-org2"), + HttpClient: &http.Client{ + Timeout: 1234509, + }, + }, subject.GetConfig()) + + require.Equal(t, "foo", subject.GetResourceName()) + require.Equal(t, types.UID("resource-uid-12345"), subject.GetResourceUID()) + require.Equal(t, "fake-client-id", subject.GetClientID()) + require.Equal(t, "fake-client-id", subject.GetClientID()) + require.Equal(t, supervisoridpv1alpha1.GitHubUsernameAttribute("fake-username-attribute"), subject.GetUsernameAttribute()) + require.Equal(t, supervisoridpv1alpha1.GitHubGroupNameAttribute("fake-group-name-attribute"), subject.GetGroupNameAttribute()) + require.Equal(t, setutil.NewCaseInsensitiveSet("fake-org", "fake-org2"), subject.GetAllowedOrganizations()) + require.Equal(t, "https://fake-authorization-url", subject.GetAuthorizationURL()) + require.Equal(t, &http.Client{ + Timeout: 1234509, + }, subject.GetConfig().HttpClient) +} + +func TestExchangeAuthcode(t *testing.T) { + const fakeGitHubAccessToken = "gho_16C7e42F292c6912E7710c838347Ae178B4a" //nolint:gosec // this is not a credential + + tests := []struct { + name string + tokenEndpointPath string + wantErr string + }{ + { + name: "happy path", + tokenEndpointPath: "/token", + }, + { + name: "when the GitHub token endpoint returns an error", + tokenEndpointPath: "/token-error", + wantErr: "error exchanging authorization code using GitHub API: oauth2: cannot fetch token: 401 Unauthorized\nResponse: some github error", + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + t.Parallel() + + testServer, testServerCA := tlsserver.TestServerIPv4(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // See documentation at https://docs.github.com/en/apps/oauth-apps/building-oauth-apps/authorizing-oauth-apps + // GitHub docs say to use a POST. + require.Equal(t, http.MethodPost, r.Method) + + // The OAuth client library happens to choose to send these headers. Asserting here for our own understanding. + require.Len(t, r.Header, 4) + require.Equal(t, "application/x-www-form-urlencoded", r.Header.Get("Content-Type")) + require.Equal(t, "gzip", r.Header.Get("Accept-Encoding")) + require.NotEmpty(t, r.Header.Get("User-Agent")) + require.NotEmpty(t, r.Header.Get("Content-Length")) + + // Get the params. + err := r.ParseForm() + require.NoError(t, err) + params := r.PostForm + require.Len(t, params, 5) + // These four params are documented by GitHub. + require.Equal(t, "fake-client-id", params.Get("client_id")) + require.Equal(t, "fake-client-secret", params.Get("client_secret")) + require.Equal(t, "https://fake-redirect-url", params.Get("redirect_uri")) + require.Equal(t, "fake-authcode", params.Get("code")) + // This param is not documented by GitHub, but is standard OAuth2. GitHub should respect or ignore it. + require.Equal(t, "authorization_code", params.Get("grant_type")) + + // The GitHub docs say that it will return a URL encoded form by default, so I assume it would set this header. + w.Header().Set("content-type", "application/x-www-form-urlencoded") + + switch r.URL.Path { + case "/token": + // Example response from GitHub docs. + responseBody := "access_token=" + fakeGitHubAccessToken + "&scope=repo%2Cgist&token_type=bearer" + w.WriteHeader(http.StatusOK) + _, err = w.Write([]byte(responseBody)) + require.NoError(t, err) + case "/token-error": + responseBody := "some github error" + w.WriteHeader(http.StatusUnauthorized) + _, err = w.Write([]byte(responseBody)) + require.NoError(t, err) + default: + t.Fatalf("tried to call provider at unexpected endpoint: %s", r.URL.Path) + } + }), nil) + testServerPool, err := cert.NewPoolFromBytes(testServerCA) + require.NoError(t, err) + + subject := New(ProviderConfig{ + OAuth2Config: &oauth2.Config{ + ClientID: "fake-client-id", + ClientSecret: "fake-client-secret", + Scopes: []string{"scope1", "scope2"}, + Endpoint: oauth2.Endpoint{ + AuthURL: "https://fake-auth-url", + TokenURL: testServer.URL + test.tokenEndpointPath, + AuthStyle: oauth2.AuthStyleInParams, + }, + }, + HttpClient: &http.Client{ + Timeout: 10 * time.Second, + Transport: &http.Transport{TLSClientConfig: &tls.Config{ + MinVersion: tls.VersionTLS12, + RootCAs: testServerPool, + }}, + }, + }) + + accessToken, err := subject.ExchangeAuthcode(context.Background(), "fake-authcode", "https://fake-redirect-url") + if test.wantErr != "" { + require.EqualError(t, err, test.wantErr) + require.Empty(t, accessToken) + } else { + require.NoError(t, err) + require.Equal(t, fakeGitHubAccessToken, accessToken) + } + }) + } +} + +func TestGetUser(t *testing.T) { + const idpDisplayName = "idp display name 😀" + const encodedIDPDisplayName = "idp+display+name+%F0%9F%98%80" + + someContext := context.Background() + + someHttpClient := &http.Client{ + Timeout: 1234509, + } + + tests := []struct { + name string + providerConfig ProviderConfig + buildGitHubClientError error + buildMockResponses func(hubInterface *mockgithubclient.MockGitHubInterface) + wantUser *upstreamprovider.GitHubUser + wantErrMsg string + wantErr error + }{ + { + name: "happy path with username=login:id", + providerConfig: ProviderConfig{ + APIBaseURL: "https://some-url", + HttpClient: someHttpClient, + UsernameAttribute: supervisoridpv1alpha1.GitHubUsernameLoginAndID, + }, + buildMockResponses: func(mockGitHubInterface *mockgithubclient.MockGitHubInterface) { + mockGitHubInterface.EXPECT().GetUserInfo(someContext).Return(&githubclient.UserInfo{ + Login: "some-github-login", + ID: "some-github-id", + }, nil) + mockGitHubInterface.EXPECT().GetOrgMembership(someContext).Return(nil, nil) + mockGitHubInterface.EXPECT().GetTeamMembership(someContext, gomock.Any()).Return(nil, nil) + }, + wantUser: &upstreamprovider.GitHubUser{ + Username: "some-github-login:some-github-id", + DownstreamSubject: fmt.Sprintf("https://some-url?idpName=%s&login=some-github-login&id=some-github-id", encodedIDPDisplayName), + }, + }, + { + name: "happy path with username=login", + providerConfig: ProviderConfig{ + APIBaseURL: "https://some-url", + HttpClient: someHttpClient, + UsernameAttribute: supervisoridpv1alpha1.GitHubUsernameLogin, + }, + buildMockResponses: func(mockGitHubInterface *mockgithubclient.MockGitHubInterface) { + mockGitHubInterface.EXPECT().GetUserInfo(someContext).Return(&githubclient.UserInfo{ + Login: "some-github-login", + ID: "some-github-id", + }, nil) + mockGitHubInterface.EXPECT().GetOrgMembership(someContext).Return(nil, nil) + mockGitHubInterface.EXPECT().GetTeamMembership(someContext, nil).Return(nil, nil) + }, + wantUser: &upstreamprovider.GitHubUser{ + Username: "some-github-login", + DownstreamSubject: fmt.Sprintf("https://some-url?idpName=%s&login=some-github-login&id=some-github-id", encodedIDPDisplayName), + }, + }, + { + name: "happy path with username=id", + providerConfig: ProviderConfig{ + APIBaseURL: "https://some-url", + HttpClient: someHttpClient, + UsernameAttribute: supervisoridpv1alpha1.GitHubUsernameID, + }, + buildMockResponses: func(mockGitHubInterface *mockgithubclient.MockGitHubInterface) { + mockGitHubInterface.EXPECT().GetUserInfo(someContext).Return(&githubclient.UserInfo{ + Login: "some-github-login", + ID: "some-github-id", + }, nil) + mockGitHubInterface.EXPECT().GetOrgMembership(someContext).Return(nil, nil) + mockGitHubInterface.EXPECT().GetTeamMembership(someContext, nil).Return(nil, nil) + }, + wantUser: &upstreamprovider.GitHubUser{ + Username: "some-github-id", + DownstreamSubject: fmt.Sprintf("https://some-url?idpName=%s&login=some-github-login&id=some-github-id", encodedIDPDisplayName), + }, + }, + { + name: "happy path with user in allowed organizations", + providerConfig: ProviderConfig{ + APIBaseURL: "https://some-url", + HttpClient: someHttpClient, + UsernameAttribute: supervisoridpv1alpha1.GitHubUsernameLoginAndID, + AllowedOrganizations: setutil.NewCaseInsensitiveSet("ALLOWED-ORG1", "ALLOWED-ORG2"), + }, + buildMockResponses: func(mockGitHubInterface *mockgithubclient.MockGitHubInterface) { + mockGitHubInterface.EXPECT().GetUserInfo(someContext).Return(&githubclient.UserInfo{ + Login: "some-github-login", + ID: "some-github-id", + }, nil) + mockGitHubInterface.EXPECT().GetOrgMembership(someContext).Return([]string{"allowed-org2"}, nil) + mockGitHubInterface.EXPECT().GetTeamMembership(someContext, setutil.NewCaseInsensitiveSet("ALLOWED-ORG1", "ALLOWED-ORG2")).Return(nil, nil) + }, + wantUser: &upstreamprovider.GitHubUser{ + Username: "some-github-login:some-github-id", + DownstreamSubject: fmt.Sprintf("https://some-url?idpName=%s&login=some-github-login&id=some-github-id", encodedIDPDisplayName), + }, + }, + { + name: "returns error when the user does not belong to the allowed organizations", + providerConfig: ProviderConfig{ + APIBaseURL: "https://some-url", + HttpClient: someHttpClient, + UsernameAttribute: supervisoridpv1alpha1.GitHubUsernameID, + AllowedOrganizations: setutil.NewCaseInsensitiveSet("allowed-org"), + }, + buildMockResponses: func(mockGitHubInterface *mockgithubclient.MockGitHubInterface) { + mockGitHubInterface.EXPECT().GetUserInfo(someContext).Return(&githubclient.UserInfo{ + Login: "some-github-login", + ID: "some-github-id", + }, nil) + mockGitHubInterface.EXPECT().GetOrgMembership(someContext).Return([]string{"disallowed-org"}, nil) + }, + wantErr: upstreamprovider.NewGitHubLoginDeniedError("user is not allowed to log in due to organization membership policy"), + }, + { + name: "happy path with groups=name", + providerConfig: ProviderConfig{ + APIBaseURL: "https://some-url", + HttpClient: someHttpClient, + UsernameAttribute: supervisoridpv1alpha1.GitHubUsernameLoginAndID, + AllowedOrganizations: setutil.NewCaseInsensitiveSet("allowed-org1", "allowed-org2"), + GroupNameAttribute: supervisoridpv1alpha1.GitHubUseTeamNameForGroupName, + }, + buildMockResponses: func(mockGitHubInterface *mockgithubclient.MockGitHubInterface) { + mockGitHubInterface.EXPECT().GetUserInfo(someContext).Return(&githubclient.UserInfo{ + Login: "some-github-login", + ID: "some-github-id", + }, nil) + mockGitHubInterface.EXPECT().GetOrgMembership(someContext).Return([]string{"allowed-org2"}, nil) + mockGitHubInterface.EXPECT().GetTeamMembership(someContext, setutil.NewCaseInsensitiveSet("allowed-org1", "allowed-org2")).Return([]githubclient.TeamInfo{ + { + Name: "org1-team1-name", + Slug: "org1-team1-slug", + Org: "org1-name", + }, + { + Name: "org1-team2-name", + Slug: "org1-team2-slug", + Org: "org1-name", + }, + { + Name: "org2-team1-name", + Slug: "org2-team1-slug", + Org: "org2-name", + }, + }, nil) + }, + wantUser: &upstreamprovider.GitHubUser{ + Username: "some-github-login:some-github-id", + Groups: []string{"org1-name/org1-team1-name", "org1-name/org1-team2-name", "org2-name/org2-team1-name"}, + DownstreamSubject: fmt.Sprintf("https://some-url?idpName=%s&login=some-github-login&id=some-github-id", encodedIDPDisplayName), + }, + }, + { + name: "happy path with groups=slug", + providerConfig: ProviderConfig{ + APIBaseURL: "https://some-url", + HttpClient: someHttpClient, + UsernameAttribute: supervisoridpv1alpha1.GitHubUsernameLoginAndID, + AllowedOrganizations: setutil.NewCaseInsensitiveSet("allowed-org1", "allowed-org2"), + GroupNameAttribute: supervisoridpv1alpha1.GitHubUseTeamSlugForGroupName, + }, + buildMockResponses: func(mockGitHubInterface *mockgithubclient.MockGitHubInterface) { + mockGitHubInterface.EXPECT().GetUserInfo(someContext).Return(&githubclient.UserInfo{ + Login: "some-github-login", + ID: "some-github-id", + }, nil) + mockGitHubInterface.EXPECT().GetOrgMembership(someContext).Return([]string{"allowed-org2"}, nil) + mockGitHubInterface.EXPECT().GetTeamMembership(someContext, setutil.NewCaseInsensitiveSet("allowed-org1", "allowed-org2")).Return([]githubclient.TeamInfo{ + { + Name: "org1-team1-name", + Slug: "org1-team1-slug", + Org: "org1-name", + }, + { + Name: "org1-team2-name", + Slug: "org1-team2-slug", + Org: "org1-name", + }, + { + Name: "org2-team1-name", + Slug: "org2-team1-slug", + Org: "org2-name", + }, + }, nil) + }, + wantUser: &upstreamprovider.GitHubUser{ + Username: "some-github-login:some-github-id", + Groups: []string{"org1-name/org1-team1-slug", "org1-name/org1-team2-slug", "org2-name/org2-team1-slug"}, + DownstreamSubject: fmt.Sprintf("https://some-url?idpName=%s&login=some-github-login&id=some-github-id", encodedIDPDisplayName), + }, + }, + { + name: "returns errors from buildGitHubClient()", + providerConfig: ProviderConfig{ + APIBaseURL: "https://some-url", + HttpClient: someHttpClient, + }, + buildGitHubClientError: errors.New("error from building a github client"), + wantErrMsg: "error from building a github client", + }, + { + name: "returns errors from githubClient.GetUserInfo()", + providerConfig: ProviderConfig{ + APIBaseURL: "https://some-url", + HttpClient: someHttpClient, + }, + buildMockResponses: func(mockGitHubInterface *mockgithubclient.MockGitHubInterface) { + mockGitHubInterface.EXPECT().GetUserInfo(someContext).Return(nil, errors.New("error from githubClient.GetUserInfo")) + }, + wantErrMsg: "error from githubClient.GetUserInfo", + }, + { + name: "returns errors from githubClient.GetOrgMembership()", + providerConfig: ProviderConfig{ + APIBaseURL: "https://some-url", + HttpClient: someHttpClient, + UsernameAttribute: supervisoridpv1alpha1.GitHubUsernameLoginAndID, + }, + buildMockResponses: func(mockGitHubInterface *mockgithubclient.MockGitHubInterface) { + mockGitHubInterface.EXPECT().GetUserInfo(someContext).Return(&githubclient.UserInfo{}, nil) + mockGitHubInterface.EXPECT().GetOrgMembership(someContext).Return(nil, errors.New("error from githubClient.GetOrgMembership")) + }, + wantErrMsg: "error from githubClient.GetOrgMembership", + }, + { + name: "returns errors from githubClient.GetTeamMembership()", + providerConfig: ProviderConfig{ + APIBaseURL: "https://some-url", + HttpClient: someHttpClient, + UsernameAttribute: supervisoridpv1alpha1.GitHubUsernameLoginAndID, + }, + buildMockResponses: func(mockGitHubInterface *mockgithubclient.MockGitHubInterface) { + mockGitHubInterface.EXPECT().GetUserInfo(someContext).Return(&githubclient.UserInfo{}, nil) + mockGitHubInterface.EXPECT().GetOrgMembership(someContext).Return(nil, nil) + mockGitHubInterface.EXPECT().GetTeamMembership(someContext, gomock.Any()).Return(nil, errors.New("error from githubClient.GetTeamMembership")) + }, + wantErrMsg: "error from githubClient.GetTeamMembership", + }, + { + name: "bad configuration: UsernameAttribute", + providerConfig: ProviderConfig{ + APIBaseURL: "https://some-url", + HttpClient: someHttpClient, + UsernameAttribute: "this-is-not-legal-value-from-the-enum", + }, + buildMockResponses: func(mockGitHubInterface *mockgithubclient.MockGitHubInterface) { + mockGitHubInterface.EXPECT().GetUserInfo(someContext).Return(&githubclient.UserInfo{ + Login: "some-github-login", + ID: "some-github-id", + }, nil) + }, + wantErrMsg: "bad configuration: unknown GitHub username attribute: this-is-not-legal-value-from-the-enum", + }, + { + name: "bad configuration: GroupNameAttribute", + providerConfig: ProviderConfig{ + APIBaseURL: "https://some-url", + HttpClient: someHttpClient, + UsernameAttribute: supervisoridpv1alpha1.GitHubUsernameLoginAndID, + GroupNameAttribute: "this-is-not-legal-value-from-the-enum", + }, + buildMockResponses: func(mockGitHubInterface *mockgithubclient.MockGitHubInterface) { + mockGitHubInterface.EXPECT().GetUserInfo(someContext).Return(&githubclient.UserInfo{ + Login: "some-github-login", + ID: "some-github-id", + }, nil) + mockGitHubInterface.EXPECT().GetOrgMembership(someContext).Return(nil, nil) + mockGitHubInterface.EXPECT().GetTeamMembership(someContext, nil).Return([]githubclient.TeamInfo{ + { + Name: "org1-team1-name", + Slug: "org1-team1-slug", + Org: "org1-name", + }, + }, nil) + }, + wantErrMsg: "bad configuration: unknown GitHub group name attribute: this-is-not-legal-value-from-the-enum", + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + t.Parallel() + + ctrl := gomock.NewController(t) + t.Cleanup(ctrl.Finish) + + accessToken := "some-opaque-github-access-token" + rand.String(8) + mockGitHubInterface := mockgithubclient.NewMockGitHubInterface(ctrl) + if test.buildMockResponses != nil { + test.buildMockResponses(mockGitHubInterface) + } + + p := New(test.providerConfig) + p.buildGitHubClient = func(httpClient *http.Client, apiBaseURL, token string) (githubclient.GitHubInterface, error) { + require.Equal(t, test.providerConfig.HttpClient, httpClient) + require.Equal(t, test.providerConfig.APIBaseURL, apiBaseURL) + require.Equal(t, accessToken, token) + + return mockGitHubInterface, test.buildGitHubClientError + } + + actualUser, actualErr := p.GetUser(context.Background(), accessToken, idpDisplayName) + + switch { + case test.wantErrMsg != "": + require.EqualError(t, actualErr, test.wantErrMsg) + require.Nil(t, actualUser) + case test.wantErr != nil: + require.Equal(t, test.wantErr, actualErr) + require.Nil(t, actualUser) + default: + require.NoError(t, actualErr) + require.Equal(t, test.wantUser, actualUser) + } + }) + } +} diff --git a/internal/upstreamldap/upstreamldap.go b/internal/upstreamldap/upstreamldap.go index 7735b231b..2821a4483 100644 --- a/internal/upstreamldap/upstreamldap.go +++ b/internal/upstreamldap/upstreamldap.go @@ -118,7 +118,7 @@ type ProviderConfig struct { GroupAttributeParsingOverrides map[string]func(*ldap.Entry) (string, error) // RefreshAttributeChecks are extra checks that attributes in a refresh response are as expected. - RefreshAttributeChecks map[string]func(*ldap.Entry, upstreamprovider.RefreshAttributes) error + RefreshAttributeChecks map[string]func(*ldap.Entry, upstreamprovider.LDAPRefreshAttributes) error } // UserSearchConfig contains information about how to search for users in the upstream LDAP IDP. @@ -186,8 +186,8 @@ func closeAndLogError(conn Conn, doingWhat string) { } } -func (p *Provider) PerformRefresh(ctx context.Context, storedRefreshAttributes upstreamprovider.RefreshAttributes, idpDisplayName string) ([]string, error) { - t := trace.FromContext(ctx).Nest("slow ldap refresh attempt", trace.Field{Key: "providerName", Value: p.GetName()}) +func (p *Provider) PerformRefresh(ctx context.Context, storedRefreshAttributes upstreamprovider.LDAPRefreshAttributes, idpDisplayName string) ([]string, error) { + t := trace.FromContext(ctx).Nest("slow ldap refresh attempt", trace.Field{Key: "providerName", Value: p.GetResourceName()}) defer t.LogIfLong(500 * time.Millisecond) // to help users debug slow LDAP searches userDN := storedRefreshAttributes.DN @@ -373,7 +373,7 @@ func (p *Provider) tlsConfig() (*tls.Config, error) { } // GetName returns a name for this upstream provider. -func (p *Provider) GetName() string { +func (p *Provider) GetResourceName() string { return p.c.Name } @@ -435,7 +435,7 @@ func (p *Provider) AuthenticateUser(ctx context.Context, username, password stri } func (p *Provider) authenticateUserImpl(ctx context.Context, username string, bindFunc func(conn Conn, foundUserDN string) error) (*authenticators.Response, bool, error) { - t := trace.FromContext(ctx).Nest("slow ldap authenticate user attempt", trace.Field{Key: "providerName", Value: p.GetName()}) + t := trace.FromContext(ctx).Nest("slow ldap authenticate user attempt", trace.Field{Key: "providerName", Value: p.GetResourceName()}) defer t.LogIfLong(500 * time.Millisecond) // to help users debug slow LDAP searches err := p.validateConfig() @@ -528,7 +528,7 @@ func (p *Provider) validateConfig() error { } func (p *Provider) SearchForDefaultNamingContext(ctx context.Context) (string, error) { - t := trace.FromContext(ctx).Nest("slow ldap attempt when searching for default naming context", trace.Field{Key: "providerName", Value: p.GetName()}) + t := trace.FromContext(ctx).Nest("slow ldap attempt when searching for default naming context", trace.Field{Key: "providerName", Value: p.GetResourceName()}) defer t.LogIfLong(500 * time.Millisecond) // to help users debug slow LDAP searches conn, err := p.dial(ctx) @@ -564,7 +564,7 @@ func (p *Provider) searchAndBindUser(conn Conn, username string, bindFunc func(c searchResult, err := conn.Search(p.userSearchRequest(username)) if err != nil { plog.All(`error searching for user`, - "upstreamName", p.GetName(), + "upstreamName", p.GetResourceName(), "username", username, "err", err, ) @@ -573,11 +573,11 @@ func (p *Provider) searchAndBindUser(conn Conn, username string, bindFunc func(c if len(searchResult.Entries) == 0 { if plog.Enabled(plog.LevelAll) { plog.All("error finding user: user not found (if this username is valid, please check the user search configuration)", - "upstreamName", p.GetName(), + "upstreamName", p.GetResourceName(), "username", username, ) } else { - plog.Debug("error finding user: user not found (cowardly avoiding printing username because log level is not 'all')", "upstreamName", p.GetName()) + plog.Debug("error finding user: user not found (cowardly avoiding printing username because log level is not 'all')", "upstreamName", p.GetResourceName()) } return nil, nil } @@ -632,7 +632,7 @@ func (p *Provider) searchAndBindUser(conn Conn, username string, bindFunc func(c err = bindFunc(conn, userEntry.DN) if err != nil { plog.DebugErr("error binding for user (if this is not the expected dn for this username, please check the user search configuration)", - err, "upstreamName", p.GetName(), "username", username, "dn", userEntry.DN) + err, "upstreamName", p.GetResourceName(), "username", username, "dn", userEntry.DN) ldapErr := &ldap.Error{} if errors.As(err, &ldapErr) && ldapErr.ResultCode == ldap.LDAPResultInvalidCredentials { return nil, nil diff --git a/internal/upstreamldap/upstreamldap_test.go b/internal/upstreamldap/upstreamldap_test.go index f7cdcadbe..49e36968e 100644 --- a/internal/upstreamldap/upstreamldap_test.go +++ b/internal/upstreamldap/upstreamldap_test.go @@ -641,8 +641,8 @@ func TestEndUserAuthentication(t *testing.T) { username: testUpstreamUsername, password: testUpstreamPassword, providerConfig: providerConfig(func(p *ProviderConfig) { - p.RefreshAttributeChecks = map[string]func(entry *ldap.Entry, attributes upstreamprovider.RefreshAttributes) error{ - "some-attribute-to-check-during-refresh": func(entry *ldap.Entry, attributes upstreamprovider.RefreshAttributes) error { + p.RefreshAttributeChecks = map[string]func(entry *ldap.Entry, attributes upstreamprovider.LDAPRefreshAttributes) error{ + "some-attribute-to-check-during-refresh": func(entry *ldap.Entry, attributes upstreamprovider.LDAPRefreshAttributes) error { return nil }, } @@ -679,8 +679,8 @@ func TestEndUserAuthentication(t *testing.T) { username: testUpstreamUsername, password: testUpstreamPassword, providerConfig: providerConfig(func(p *ProviderConfig) { - p.RefreshAttributeChecks = map[string]func(entry *ldap.Entry, attributes upstreamprovider.RefreshAttributes) error{ - "some-attribute-to-check-during-refresh": func(entry *ldap.Entry, attributes upstreamprovider.RefreshAttributes) error { + p.RefreshAttributeChecks = map[string]func(entry *ldap.Entry, attributes upstreamprovider.LDAPRefreshAttributes) error{ + "some-attribute-to-check-during-refresh": func(entry *ldap.Entry, attributes upstreamprovider.LDAPRefreshAttributes) error { return nil }, } @@ -1527,8 +1527,8 @@ func TestUpstreamRefresh(t *testing.T) { Filter: testGroupSearchFilter, GroupNameAttribute: testGroupSearchGroupNameAttribute, }, - RefreshAttributeChecks: map[string]func(*ldap.Entry, upstreamprovider.RefreshAttributes) error{ - pwdLastSetAttribute: func(*ldap.Entry, upstreamprovider.RefreshAttributes) error { return nil }, + RefreshAttributeChecks: map[string]func(*ldap.Entry, upstreamprovider.LDAPRefreshAttributes) error{ + pwdLastSetAttribute: func(*ldap.Entry, upstreamprovider.LDAPRefreshAttributes) error { return nil }, }, } if editFunc != nil { @@ -2124,8 +2124,8 @@ func TestUpstreamRefresh(t *testing.T) { { name: "search result has a changed pwdLastSet value", providerConfig: providerConfig(func(p *ProviderConfig) { - p.RefreshAttributeChecks = map[string]func(*ldap.Entry, upstreamprovider.RefreshAttributes) error{ - pwdLastSetAttribute: func(*ldap.Entry, upstreamprovider.RefreshAttributes) error { + p.RefreshAttributeChecks = map[string]func(*ldap.Entry, upstreamprovider.LDAPRefreshAttributes) error{ + pwdLastSetAttribute: func(*ldap.Entry, upstreamprovider.LDAPRefreshAttributes) error { return errors.New(`value for attribute "pwdLastSet" has changed since initial value at login`) }, } @@ -2201,7 +2201,7 @@ func TestUpstreamRefresh(t *testing.T) { "ldaps://ldap.example.com:8443?base=some-upstream-user-base-dn&idpName=%s&sub=c29tZS11cHN0cmVhbS11aWQtdmFsdWU", testUpstreamName, ) - groups, err := ldapProvider.PerformRefresh(context.Background(), upstreamprovider.RefreshAttributes{ + groups, err := ldapProvider.PerformRefresh(context.Background(), upstreamprovider.LDAPRefreshAttributes{ Username: testUserSearchResultUsernameAttributeValue, Subject: subject, DN: tt.refreshUserDN, diff --git a/internal/upstreamoidc/upstreamoidc.go b/internal/upstreamoidc/upstreamoidc.go index 03ab5b498..55581b608 100644 --- a/internal/upstreamoidc/upstreamoidc.go +++ b/internal/upstreamoidc/upstreamoidc.go @@ -84,7 +84,7 @@ func (p *ProviderConfig) GetAdditionalClaimMappings() map[string]string { return p.AdditionalClaimMappings } -func (p *ProviderConfig) GetName() string { +func (p *ProviderConfig) GetResourceName() string { return p.Name } diff --git a/internal/upstreamoidc/upstreamoidc_test.go b/internal/upstreamoidc/upstreamoidc_test.go index 2521d3016..2ce45cdbb 100644 --- a/internal/upstreamoidc/upstreamoidc_test.go +++ b/internal/upstreamoidc/upstreamoidc_test.go @@ -47,7 +47,7 @@ func TestProviderConfig(t *testing.T) { rawClaims: []byte(`{"userinfo_endpoint": "https://example.com/userinfo"}`), }, } - require.Equal(t, "test-name", p.GetName()) + require.Equal(t, "test-name", p.GetResourceName()) require.Equal(t, "test-client-id", p.GetClientID()) require.Equal(t, "https://example.com", p.GetAuthorizationURL().String()) require.ElementsMatch(t, []string{"scope1", "scope2"}, p.GetScopes()) diff --git a/proposals/1859_github-auth/README.md b/proposals/1859_github-auth/README.md index c63a7052e..a0bacdbde 100644 --- a/proposals/1859_github-auth/README.md +++ b/proposals/1859_github-auth/README.md @@ -1,7 +1,7 @@ --- title: "Authenticating Users via GitHub" authors: [ "@cfryanr" ] -status: "accepted" +status: "partially-implemented" sponsor: [ ] approval_date: "March 27, 2024" --- @@ -77,6 +77,7 @@ GitHub offers a REST API which can be used to find out about, among other things [team memberships of the currently authenticated user](https://docs.github.com/en/rest/teams/teams?apiVersion=2022-11-28#list-teams-for-the-authenticated-user) (`/user/teams`). These are the only three GitHub APIs that would be used by Pinniped. +Note that the path to these APIs is slightly different for GitHub Enterprise. Access tokens from both types of OAuth 2.0 clients and both types PATs can be used to call the GitHub APIs on behalf of a GitHub user. @@ -280,7 +281,7 @@ The status conditions will be populated by a controller. The conditions will inc - `HostValid`. This is not a URL, so it must not contain `://`. It must be parsable as defined by our `endpointaddr.Parse()` helper function. - `TLSConfigurationValid`. The CA bundle can be parsed as a CA bundle. - `OrganizationsPolicyValid`. Use the same logic and same error text in CEL validations for this field, and repeat it in the controller in case an old version of Kubernetes is being used. -- `ClientCredentialsValid`. This is what we call the same check in the OIDCIdentityProvider's status. +- `ClientCredentialsSecretValid`. The specified Secret must be found and must have the expected type and key/value pairs. - `GitHubConnectionValid`. Report if the host can be dialed with TLS verification. (In LDAPIdentityProvider we called this `LDAPConnectionValid`.) #### Web Browser-based Authentication using GitHubIdentityProvider @@ -556,18 +557,16 @@ None. or just one specific GitHubIdentityProvider in that FederationDomain? - Would it be helpful to offer a `GET` endpoint to list current PAT consents? What would a user do with this information? -- Should we also reduce the lifetime of the Supervisor-issued refresh tokens? This would be a signal to the client +- For PAT-base auth, should we also reduce the lifetime of the Supervisor-issued refresh tokens? This would be a signal to the client that the token is not going to work. Would this help the Pinniped CLI remove stale entries from the session cache file more quickly? - For the consent CLI commands, what if the kubeconfig's exec plugin is a path to a different CLI? It could be a different path to a different version of the Pinniped CLI, or it could be a different CLI entirely (like the `tanzu` CLI). Does this matter? -- Are the three GitHub API endpoints that we intend to use different for GitHub Enterprise Server (on-prem)? - Do they have different paths or different inputs and outputs? Need to check the GitHub docs. ## Answered Questions -- Does the GitHub Identity Provider need `additionalClaimMappings`? No. This feature was only added for +- Does the GitHubIdentityProvider need `additionalClaimMappings`? No. This feature was only added for OIDCIdentityProvider, and not yet added for other identity provider types. Please raise an issue in this repo if you need this feature. @@ -579,3 +578,9 @@ Community contributions to the effort would be welcomed. Contact the maintainers Implementing browser-based authentication will happen first. It is simpler and is a pre-requisite for the PAT consent feature for CLI-based authentication. + +### Implementation Progress + +As of June 2024, browser-based authentication using GitHub has been implemented. +This includes the GitHubIdentityProvider CRD except for the `spec.allowAuthentication.personalAccessTokens` section. +Authentication without a web browser using personal access tokens is not yet implemented. diff --git a/site/config.yaml b/site/config.yaml index 68b46e355..4706eab7e 100644 --- a/site/config.yaml +++ b/site/config.yaml @@ -7,7 +7,7 @@ params: github_url: "https://github.com/vmware-tanzu/pinniped" slack_url: "https://go.pinniped.dev/community/slack" community_url: "https://go.pinniped.dev/community" - latest_version: v0.30.0 + latest_version: v0.31.0 latest_codegen_version: 1.30 pygmentsCodefences: true pygmentsStyle: "pygments" diff --git a/site/content/docs/background/architecture.md b/site/content/docs/background/architecture.md index 633fd8288..7238b619f 100644 --- a/site/content/docs/background/architecture.md +++ b/site/content/docs/background/architecture.md @@ -48,7 +48,7 @@ Pinniped supports the following IDPs. 1. Any Active Directory identity provider (via LDAP). The -[`idp.supervisor.pinniped.dev`](https://github.com/vmware-tanzu/pinniped/blob/main/generated/{{< latestcodegenversion >}}/README.adoc#k8s-api-idp-supervisor-pinniped-dev-v1alpha1) +[`idp.supervisor.pinniped.dev`](https://github.com/vmware-tanzu/pinniped/blob/main/generated/latest/README.adoc#k8s-api-idp-supervisor-pinniped-dev-v1alpha1) API group contains the Kubernetes custom resources that configure the Pinniped Supervisor's upstream IDPs. @@ -83,7 +83,7 @@ Pinniped supports the following authenticator types. set on the `kube-apiserver` process. The -[`authentication.concierge.pinniped.dev`](https://github.com/vmware-tanzu/pinniped/blob/main/generated/{{< latestcodegenversion >}}/README.adoc#k8s-api-authentication-concierge-pinniped-dev-v1alpha1) +[`authentication.concierge.pinniped.dev`](https://github.com/vmware-tanzu/pinniped/blob/main/generated/latest/README.adoc#k8s-api-authentication-concierge-pinniped-dev-v1alpha1) API group contains the Kubernetes custom resources that configure the Pinniped Concierge's authenticators. diff --git a/site/content/docs/howto/cicd.md b/site/content/docs/howto/cicd.md index df39685e4..87fbddf24 100644 --- a/site/content/docs/howto/cicd.md +++ b/site/content/docs/howto/cicd.md @@ -47,7 +47,7 @@ on each cluster. and make those kubeconfigs available to CI/CD * Be sure to use `pinniped get kubeconfig` with option `--upstream-identity-provider-flow=cli_password` to authenticate non-interactively (without a browser) * When using OIDC, the optional CLI-based flow must be enabled by the administrator in the OIDCIdentityProvider configuration before use - (see `allowPasswordGrant` in the [API docs](https://github.com/vmware-tanzu/pinniped/blob/main/generated/{{< latestcodegenversion >}}/README.adoc#oidcauthorizationconfig) for more details). + (see `allowPasswordGrant` in the [API docs](https://github.com/vmware-tanzu/pinniped/blob/main/generated/latest/README.adoc#oidcauthorizationconfig) for more details). 2. A CI/CD admin should make the non-human user account credentials available to CI/CD tasks 3. Each CI/CD task should set the environment variables `PINNIPED_USERNAME` and `PINNIPED_PASSWORD` for the `kubectl` process to avoid the interactive prompts. The values should be provided from the non-human user account credentials. diff --git a/site/content/docs/howto/concierge/configure-concierge-supervisor-jwt.md b/site/content/docs/howto/concierge/configure-concierge-supervisor-jwt.md index a478b856a..d9089775b 100644 --- a/site/content/docs/howto/concierge/configure-concierge-supervisor-jwt.md +++ b/site/content/docs/howto/concierge/configure-concierge-supervisor-jwt.md @@ -28,7 +28,7 @@ If you would rather not use the Supervisor, you may want to [configure the Conci This how-to guide assumes that you have already [installed the Pinniped Supervisor]({{< ref "install-supervisor" >}}) with working ingress, and that you have [configured a FederationDomain to issue tokens for your downstream clusters]({{< ref "configure-supervisor" >}}). -It also assumes that you have configured an `OIDCIdentityProvider`, `LDAPIdentityProvider`, or `ActiveDirectoryIdentityProvider` for the Supervisor as the source of your user's identities. +It also assumes that you have configured an `OIDCIdentityProvider`, `LDAPIdentityProvider`, `ActiveDirectoryIdentityProvider`, or `GitHubIdentityProvider` for the Supervisor as the source of your user's identities. Various examples of configuring these resources can be found in these guides. It also assumes that you have already [installed the Pinniped Concierge]({{< ref "install-concierge" >}}) diff --git a/site/content/docs/howto/configure-auth-for-webapps.md b/site/content/docs/howto/configure-auth-for-webapps.md index cb98c01f7..3074fd628 100644 --- a/site/content/docs/howto/configure-auth-for-webapps.md +++ b/site/content/docs/howto/configure-auth-for-webapps.md @@ -376,7 +376,7 @@ scenes is actually served by the Pinniped Concierge. It can be accessed just lik not require any authentication on the request. The details of the request and response formats are documented in the -[API docs](https://github.com/vmware-tanzu/pinniped/blob/main/generated/{{< latestcodegenversion >}}/README.adoc#tokencredentialrequest). +[API docs](https://github.com/vmware-tanzu/pinniped/blob/main/generated/latest/README.adoc#tokencredentialrequest). Here is a sample YAML representation of a request: diff --git a/site/content/docs/howto/login.md b/site/content/docs/howto/login.md index 57843effe..08e862f3c 100644 --- a/site/content/docs/howto/login.md +++ b/site/content/docs/howto/login.md @@ -18,7 +18,7 @@ This how-to guide assumes that you have already configured the following Pinnipe then you have already: 1. [Installed the Pinniped Supervisor]({{< ref "install-supervisor" >}}) with working ingress. 1. [Configured a FederationDomain to issue tokens for your downstream clusters]({{< ref "configure-supervisor" >}}). - 1. Configured an `OIDCIdentityProvider`, `LDAPIdentityProvider`, or `ActiveDirectoryIdentityProvider` for the Supervisor as the source of your user's identities. + 1. Configured an `OIDCIdentityProvider`, `LDAPIdentityProvider`, `ActiveDirectoryIdentityProvider`, or `GitHubIdentityProvider` for the Supervisor as the source of your user's identities. Various examples of configuring these resources can be found in these guides. 1. In each cluster for which you would like to use Pinniped for authentication, you have [installed the Concierge]({{< ref "install-concierge" >}}). 1. In each cluster's Concierge, you have configured an authenticator. For example, if you are using the Pinniped Supervisor, @@ -73,7 +73,7 @@ The new Pinniped-compatible kubeconfig YAML will be output as stdout, and can be Various default behaviors of `pinniped get kubeconfig` can be overridden using [its command-line options]({{< ref "cli" >}}). One flag of note is `--upstream-identity-provider-flow browser_authcode` to choose end-user `kubectl` login via a web browser -(the default for OIDCIdentityProviders), and `--upstream-identity-provider-flow cli_password` to choose end-user `kubectl` +(the default for OIDCIdentityProviders and GitHubIdentityProviders), and `--upstream-identity-provider-flow cli_password` to choose end-user `kubectl` login via CLI username/password prompts (the default for LDAPIdentityProviders and ActiveDirectoryIdentityProviders). If the cluster is using a Pinniped Supervisor's FederationDomain to provide authentication services, @@ -125,7 +125,7 @@ will depend on which type of identity provider was configured. `kubectl` process to avoid the interactive prompts. Note that the optional CLI-based flow must be enabled by the administrator in the OIDCIdentityProvider configuration before use (see `allowPasswordGrant` in the - [API docs](https://github.com/vmware-tanzu/pinniped/blob/main/generated/{{< latestcodegenversion >}}/README.adoc#oidcauthorizationconfig) + [API docs](https://github.com/vmware-tanzu/pinniped/blob/main/generated/latest/README.adoc#oidcauthorizationconfig) for more details). - For LDAP and Active Directory identity providers, there are also two supported client flows: diff --git a/site/content/docs/howto/supervisor/configure-supervisor-federationdomain-idps.md b/site/content/docs/howto/supervisor/configure-supervisor-federationdomain-idps.md index 7198912c4..330f18ea4 100644 --- a/site/content/docs/howto/supervisor/configure-supervisor-federationdomain-idps.md +++ b/site/content/docs/howto/supervisor/configure-supervisor-federationdomain-idps.md @@ -20,7 +20,7 @@ This how-to guide assumes that you have already [installed the Pinniped Supervis and have already read the guide about how to [configure the Supervisor as an OIDC issuer]({{< ref "configure-supervisor" >}}). This guide focuses on the use of the `spec.identityProviders` setting on the -[FederationDomain](https://github.com/vmware-tanzu/pinniped/blob/main/generated/{{< latestcodegenversion >}}/README.adoc#federationdomain) +[FederationDomain](https://github.com/vmware-tanzu/pinniped/blob/main/generated/latest/README.adoc#federationdomain) resource. Note that the `spec.identityProviders` setting on the FederationDomain resource was added in v0.26.0 of Pinniped. @@ -29,7 +29,7 @@ This guide assumes that you are using at least that version. ## Summary External identity providers may be configured in the Supervisor by creating OIDCIdentityProvider, -ActiveDirectoryIdentityProvider, or LDAPIdentityProvider resources in the same namespace as the Supervisor. +ActiveDirectoryIdentityProvider, LDAPIdentityProvider, or GitHubIdentityProvider resources in the same namespace as the Supervisor. There are two ways to configure which of these external identity providers shall be used by a FederationDomain. @@ -37,7 +37,7 @@ There are two ways to configure which of these external identity providers shall the one and only identity provider that is configured in the same namespace. This provides backwards compatibility with older configurations of Supervisors from before the `spec.identityProviders` setting was added to the FederationDomain resource. There must be exactly one OIDCIdentityProvider, - ActiveDirectoryIdentityProvider, or LDAPIdentityProvider resource in the same namespace as the Supervisor. + ActiveDirectoryIdentityProvider, LDAPIdentityProvider, or GitHubIdentityProvider resource in the same namespace as the Supervisor. If there are no identity provider resources, or if there are more than one, then the FederationDomain will not allow any users to authenticate, and a error message will be shown in its `status`. @@ -133,8 +133,8 @@ and group names. ## Identity transformations and policies -When a user authenticates, the configuration of the OIDCIdentityProvider, ActiveDirectoryIdentityProvider, or -LDAPIdentityProvider resource determines how the user's username and group names will be extracted from the external +When a user authenticates, the configuration of the OIDCIdentityProvider, ActiveDirectoryIdentityProvider, +LDAPIdentityProvider, or GitHubIdentityProvider resource determines how the user's username and group names will be extracted from the external identity provider in a protocol-specific way (e.g. via OIDC ID token claims or LDAP record attributes). Then, operating on the username and group names extracted from the external IDP: @@ -217,7 +217,7 @@ act as living documentation for your fellow administrators, and also act as unit Each example declares inputs for the whole pipeline of expressions, and also declares the expected results of the entire pipeline running on those inputs. The inputs are examples of the username and list of group names that might -be determined by the related OIDCIdentityProvider, ActiveDirectoryIdentityProvider, or LDAPIdentityProvider resource. +be determined by the related OIDCIdentityProvider, ActiveDirectoryIdentityProvider, LDAPIdentityProvider, or GitHubIdentityProvider resource. The expected outputs are the username and list of group names, or the authentication rejection, for which your pipeline should result upon the given inputs. @@ -230,7 +230,7 @@ The following example is contrived to demonstrate every feature of the `transfor (constants, expressions, and examples). It is likely more complex than a typical configuration. Documentation for each of the fields shown below can be found in the API docs for the -[FederationDomain](https://github.com/vmware-tanzu/pinniped/blob/main/generated/{{< latestcodegenversion >}}/README.adoc#federationdomain) +[FederationDomain](https://github.com/vmware-tanzu/pinniped/blob/main/generated/latest/README.adoc#federationdomain) resource. ```yaml diff --git a/site/content/docs/howto/supervisor/configure-supervisor-with-activedirectory.md b/site/content/docs/howto/supervisor/configure-supervisor-with-activedirectory.md index ca48fc1b5..55c8fc416 100644 --- a/site/content/docs/howto/supervisor/configure-supervisor-with-activedirectory.md +++ b/site/content/docs/howto/supervisor/configure-supervisor-with-activedirectory.md @@ -24,7 +24,7 @@ and that you have [configured a FederationDomain to issue tokens for your downst ## Configure the Supervisor cluster -Create an [ActiveDirectoryIdentityProvider](https://github.com/vmware-tanzu/pinniped/blob/main/generated/{{< latestcodegenversion >}}/README.adoc#activedirectoryidentityprovider) in the same namespace as the Supervisor. +Create an [ActiveDirectoryIdentityProvider](https://github.com/vmware-tanzu/pinniped/blob/main/generated/latest/README.adoc#activedirectoryidentityprovider) in the same namespace as the Supervisor. ### ActiveDirectoryIdentityProvider with default options diff --git a/site/content/docs/howto/supervisor/configure-supervisor-with-auth0.md b/site/content/docs/howto/supervisor/configure-supervisor-with-auth0.md index 52effc3b5..7a463b1bd 100644 --- a/site/content/docs/howto/supervisor/configure-supervisor-with-auth0.md +++ b/site/content/docs/howto/supervisor/configure-supervisor-with-auth0.md @@ -71,7 +71,7 @@ To configure your Kubernetes authorization, please see [how-to login]({{< ref "l ## Configure the Supervisor -Create an [OIDCIdentityProvider](https://github.com/vmware-tanzu/pinniped/blob/main/generated/{{< latestcodegenversion >}}/README.adoc#oidcidentityprovider) in the same namespace as the Supervisor. +Create an [OIDCIdentityProvider](https://github.com/vmware-tanzu/pinniped/blob/main/generated/latest/README.adoc#oidcidentityprovider) in the same namespace as the Supervisor. For example, this OIDCIdentityProvider uses Auth0's `email` claim as the Kubernetes username: diff --git a/site/content/docs/howto/supervisor/configure-supervisor-with-azuread.md b/site/content/docs/howto/supervisor/configure-supervisor-with-azuread.md index f28af6963..50966e9b4 100644 --- a/site/content/docs/howto/supervisor/configure-supervisor-with-azuread.md +++ b/site/content/docs/howto/supervisor/configure-supervisor-with-azuread.md @@ -46,7 +46,7 @@ For example, to create a tenant: ## Configure the Supervisor -Create an [OIDCIdentityProvider](https://github.com/vmware-tanzu/pinniped/blob/main/generated/{{< latestcodegenversion >}}/README.adoc#oidcidentityprovider) +Create an [OIDCIdentityProvider](https://github.com/vmware-tanzu/pinniped/blob/main/generated/latest/README.adoc#oidcidentityprovider) in the same namespace as the Supervisor. 1. In the [Azure portal](portal.azure.com), navigate to _Home_ > _Azure Active Directory_ > _App Registrations_. diff --git a/site/content/docs/howto/supervisor/configure-supervisor-with-dex.md b/site/content/docs/howto/supervisor/configure-supervisor-with-dex.md index afb81a34d..db5a8879c 100644 --- a/site/content/docs/howto/supervisor/configure-supervisor-with-dex.md +++ b/site/content/docs/howto/supervisor/configure-supervisor-with-dex.md @@ -73,7 +73,7 @@ staticClients: ## Configure the Supervisor -Create an [OIDCIdentityProvider](https://github.com/vmware-tanzu/pinniped/blob/main/generated/{{< latestcodegenversion >}}/README.adoc#oidcidentityprovider) resource in the same namespace as the Supervisor. +Create an [OIDCIdentityProvider](https://github.com/vmware-tanzu/pinniped/blob/main/generated/latest/README.adoc#oidcidentityprovider) resource in the same namespace as the Supervisor. For example, the following OIDCIdentityProvider and the corresponding Secret use Dex's `email` claim as the Kubernetes username: diff --git a/site/content/docs/howto/supervisor/configure-supervisor-with-github.md b/site/content/docs/howto/supervisor/configure-supervisor-with-github.md new file mode 100644 index 000000000..60d85bed6 --- /dev/null +++ b/site/content/docs/howto/supervisor/configure-supervisor-with-github.md @@ -0,0 +1,323 @@ +--- +title: Configure the Pinniped Supervisor to use GitHub as an identity provider +description: Set up the Pinniped Supervisor to use GitHub as an identity provider. +cascade: + layout: docs +menu: + docs: + name: With GitHub + weight: 80 + parent: howto-configure-supervisor +aliases: + - /docs/howto/configure-supervisor-with-github/ +--- +The Supervisor is an [OpenID Connect (OIDC)](https://openid.net/connect/) issuer that supports connecting +"upstream" identity providers to many "downstream" cluster clients. + +This guide shows you how to configure the Supervisor so that users can authenticate to their Kubernetes +cluster using their credentials from [GitHub.com](https://github.com) or [GitHub enterprise server](https://docs.github.com/en/enterprise-server@latest/admin/overview/about-github-enterprise-server). + +## Prerequisites + +This how-to guide assumes that you have already [installed the Pinniped Supervisor]({{< ref "install-supervisor" >}}) with working ingress, +and that you have [configured a FederationDomain to issue tokens for your downstream clusters]({{< ref "configure-supervisor" >}}). + +## GitHub Apps vs. GitHub OAuth Apps + +The Pinniped Supervisor needs the client ID and client secret from either a GitHub App or a GitHub OAuth App. + +GitHub recommends that you use a GitHub App instead of a GitHub OAuth App. +The GitHub App feature is newer and has a more fully featured permission model. +The Pinniped Supervisor supports both. + +The instructions below reference the steps needed to configure a GitHub App or GitHub OAuth2 App on https://github.com at the time of writing. +GitHub UI and documentation changes frequently and may not exactly match the steps below. +Please submit a PR at the [Pinniped repo](https://github.com/vmware-tanzu/pinniped) to resolve any discrepancies. + +## Alternative 1: Create a GitHub App + +GitHub Applications can be created either in your personal profile, or directly within an organization. +The steps to create a GitHub Application for Pinniped integration are the same, but the created application +must be installed into an organization to order to see whether a user belongs to that organization and to which teams that user belongs. + +The Pinniped team recommends that the GitHub app be created within an organization, so that management of the application belongs to a team of organization admins. + +### Create the GitHub App + +To create the GitHub App within an organization (recommended), start at the organization profile, then Settings > Developer Settings > GitHub Apps > New GitHub App. +To create the GitHub App within your profile, click your user icon, then Settings > Developer Settings > GitHub Apps > New GitHub App. + +In the section called "Register new GitHub App" + +* Provide a name for your application. + This name should uniquely identify the realm of clusters and applications to which this Pinniped Supervisor permits access. + Provide a description if desired. + +* GitHub requires a `Homepage URL`, but the Pinniped Supervisor does not have such a home page. + You could provide a link to an internal company help page or perhaps https://pinniped.dev/. + No user credentials or information will be sent from GitHub to this `Homepage URL`. + +In the section called "Identifying and authorizing users" + +* For `Callback URL`, provide the Pinniped Supervisor issuer URL suffixed with `/callback`. + The issuer URL will be configured on the `FederationDomain` at `spec.issuer`. + For example, if the issuer URL is `https://example.com/some/path`, the `Callback URL` must be `https://example.com/some/path/callback`. + It is recommended to have only one callback URL for each GitHub App. + Register different GitHub apps for different Pinniped Supervisors. + +* Check `Expire user authorization tokens` to ensure that access tokens expire. + For more information, see [here](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/token-expiration-and-revocation#user-token-expired-due-to-github-app-configuration). + +* Do not check `Request user authorization (OAuth) during installation`, otherwise GitHub will redirect users to the Pinniped Supervisor for login during application installation. + Pinniped Supervisor will reject these unexpected login requests. + +* Do not check `Enable Device Flow`, since Pinniped Supervisor does not use this flow. + +In the section called "Post installation" + +* Leave all settings blank, such as `Setup URL` and `Redirect on update`. + +In the section called "Webhook" + +* Do not check `Active`, since Pinniped Supervisor does not support GitHub's webhooks. + +In the section called "Permissions" + +* The only permission needed is "Read Access to Members", found under Organization Permissions > Members (Organization members and teams) > Read-Only. + This is necessary for Pinniped Supervisor to obtain the team membership information, used to provide user groups to Kubernetes RBAC. + For more information about this permission, see [here](https://docs.github.com/en/rest/authentication/permissions-required-for-github-apps#organization-permissions-for-members). + +* For `Where can this GitHub App be installed?`, select `Any account (Allow this GitHub App to be installed by any user or organization)`. + This will allow this GitHub Application to be installed into GitHub organizations so that Pinniped Supervisor can see the user and team membership within those organizations. + +Click `Create GitHub App`. + +### Get the client ID and client secret for the new GitHub App + +On the GitHub App's General settings page, click the button to generate a new client secret. Copy the secret. +It is needed for a later step, and it will never be shown again. + +On the same page, also copy the value of the "Client ID" (note: this is different from the "App ID"). +This will also be needed in a later step. + +### Install the app into an organization + +The GitHub App for the Pinniped Supervisor must be installed into an organization in order for Pinniped to see user and team membership within that organization. + +If you created the GitHub App in your personal profile settings, request installation of your GitHub App into an organization to which you belong. +Please see the [GitHub documentation](https://docs.github.com/en/apps/using-github-apps/requesting-a-github-app-from-your-organization-owner). + +As an organization owner, you can install the GitHub App into your organization. +Additionally, you can approve the installation requests submitted by members of the organization. +For more information, see the [GitHub documentation](https://docs.github.com/en/apps/using-github-apps/installing-a-github-app-from-a-third-party). + +Note that these steps will be slightly different depending on whether the application was created within your personal account or on an organization. + +## Alternative 2: Create a GitHub OAuth App + +GitHub OAuth Apps can be created either in your personal profile, or directly within an organization. +The steps to create a GitHub OAuth App for Pinniped integration are the same, but the created application +must be approved by an organization to order to see whether a user belongs to that organization and to which teams that user belongs. + +The Pinniped team recommends that the GitHub OAuth app be created within an organization, so that management of the application belongs to a team of organization admins. + +### Create the OAuth App + +To create the GitHub OAuth App within an organization (recommended), start at the organization profile, then Settings > Developer Settings > OAuth Apps > Register an Application. +To create the GitHub OAuth App within your profile, click your user icon, then Settings > Developer Settings > OAuth Apps > New OAuth App. + +Fill out the form: + +* Provide a name for your application. + This name should uniquely identify the realm of clusters and applications to which this Pinniped Supervisor permits access. + Provide a description if desired. + +* GitHub requires a `Homepage URL`, but the Pinniped Supervisor does not have such a home page. + You could provide a link to an internal company help page or perhaps https://pinniped.dev/. + No user credentials or information will be sent from GitHub to this `Homepage URL`. + +* For `Authorization callback URL`, provide the Pinniped Supervisor issuer URL suffixed with `/callback`. + The issuer URL will be configured on the `FederationDomain` at `spec.issuer`. + For example, if the issuer URL is `https://example.com/some/path`, the `Callback URL` must be `https://example.com/some/path/callback`. + It is recommended to have only one callback URL for each GitHub OAuth App. + Register different GitHub OAuth apps for different Pinniped Supervisors. + +* Do not check `Enable Device Flow`, since Pinniped Supervisor does not use this flow. + +Click `Register Application`. + +### Get the client ID and client secret for the new GitHub OAuth App + +On the GitHub App's General settings page, click the button to generate a new client secret. Copy the secret. +It is needed for a later step, and it will never be shown again. + +On the same page, also copy the value of the "Client ID". +This will also be needed in a later step. + +### Approve the OAuth App for an organization + +The GitHub OAuth App for the Pinniped Supervisor must be approved by an organization in order for Pinniped to see user and team membership within that organization. +The organization must allow the GitHub OAuth app to access its resources. + +The creator of the GitHub OAuth App must request approval from the organization owner. See the +[GitHub documentation for requesting approval](https://docs.github.com/en/account-and-profile/setting-up-and-managing-your-personal-account-on-github/managing-your-membership-in-organizations/requesting-organization-approval-for-oauth-apps). + +How the approval works depends on whether the organization has enabled or disabled OAuth app restrictions. See the +[GitHub documentation for OAuth app restrictions](https://docs.github.com/en/organizations/managing-oauth-access-to-your-organizations-data/about-oauth-app-access-restrictions). +When OAuth app restrictions are enabled, then the organization owner must approve the app. See the +[GitHub documentation for approving an OAuth app](https://docs.github.com/en/organizations/managing-oauth-access-to-your-organizations-data/approving-oauth-apps-for-your-organization). + +## Configure the Supervisor + +Create a [GitHubIdentityProvider](https://github.com/vmware-tanzu/pinniped/blob/main/generated/{{< latestcodegenversion >}}/README.adoc#githubidentityprovider) in the same namespace as the Supervisor. + +The simplest example uses https://github.com as the source of identity. +Note that you do not need to explicitly specify a GitHub host since `github.com` is the default. +This example allows any user with a GitHub account to log in. +You may prefer to limit which users may authenticate by GitHub organization or team membership. +See the following examples for more information about limiting which users can authenticate. + +```yaml +apiVersion: idp.supervisor.pinniped.dev/v1alpha1 +kind: GitHubIdentityProvider +metadata: + namespace: pinniped-supervisor + name: github-dot-com +spec: + client: + secretName: github-dot-com-client-credentials + allowAuthentication: + organizations: + policy: AllGitHubUsers +--- +apiVersion: v1 +kind: Secret +type: secrets.pinniped.dev/github-client +metadata: + namespace: pinniped-supervisor + name: github-dot-com-client-credentials +stringData: + # The "Client ID" from the GitHub App or GitHub OAuth App. + clientID: "" + # The "Client secret" from the GitHub App or GitHub OAuth App. + clientSecret: "" +``` + +For another example, let's fill out all available fields. + +```yaml +apiVersion: idp.supervisor.pinniped.dev/v1alpha1 +kind: GitHubIdentityProvider +metadata: + namespace: pinniped-supervisor + name: github-enterprise +spec: + githubAPI: + # This field is only required when using GitHub Enterprise Server. + # Only the hostname or IP address and optional port, without the protocol. + # Pinniped will always use HTTPS. + host: github.enterprise.tld + tls: + # This field is usually only used for GitHub Enterprise Server. + # Specify the CA certificate of the server as a + # base64-encoded PEM bundle. + certificateAuthorityData: LS0tLS1CRUdJTiBDRVJUSUZJQ0FU.... + + client: + secretName: github-enterprise-client-credentials + + allowAuthentication: + # "policy: OnlyUsersFromAllowedOrganizations" restricts authentication to only + # those users who belong to at least one of the "allowed" organizations. + # Additionally, their groups as presented to K8s will only reflect team + # membership within these organizations. + organizations: + policy: OnlyUsersFromAllowedOrganizations + allowed: + - my-enterprise-organization + - my-other-organization + + claims: + # This field chooses how the username will be presented to K8s. + # Allowed values are "id", "login", or "login:id". The login and id attributes + # are taken from the results of the GitHub "/user" API endpoint. + # See https://docs.github.com/en/rest/users/users. + # Using "id" or "login:id" is recommended because GitHub users can change their + # own login name, but cannot change their numeric ID. + username: "login:id" + # This field chooses how the team names will be presented to K8s as group names. + # Allowed values are "name" or "slug". The name and slug attributes + # are taken from the results of the GitHub "/user/teams" API endpoint. + # See https://docs.github.com/en/rest/teams/teams. + # E.g. for a team named "Kube admins!", the name will be "Kube admins!" + # while the slugs will be "kube-admins". + groups: slug + +--- +apiVersion: v1 +kind: Secret +type: secrets.pinniped.dev/github-client +metadata: + namespace: pinniped-supervisor + name: github-enterprise-client-credentials +stringData: + # The "Client ID" from the GitHub App or GitHub OAuth App. + clientID: "" + # The "Client secret" from the GitHub App or GitHub OAuth App. + clientSecret: "" +``` + +Once your GitHubIdentityProvider has been created, you can validate your configuration by running: + +```shell +kubectl describe GitHubIdentityProvider -n pinniped-supervisor +``` + +Look at the `status` field. If it was configured correctly, you should see `status.phase: Ready`. +Otherwise, inspect the `status.conditions` array for more information. + +## Organization and team membership visibility + +Pinniped may not be able to see which organizations to which a user belongs, or which teams to which a user +belongs within an organization. When Pinniped is configured to restrict authentication by organization membership, it will reject a user's +authentication when it cannot see that the user belongs to one of the required organizations. +Furthermore, the user's team memberships will only be presented to Kubernetes as group names for those +teams that Pinniped is allowed to see. + +Which organizations and teams are returned by the GitHub API is controlled by the GitHub App or GitHub OAuth App that you configure. +See the documentation above for installing and/or approving the GitHub App or GitHub OAuth App for your GitHub organization. + +## Additional authentication restrictions + +The GitHubIdentityProvider specification permits restricting authentication based on organization membership. +It's possible to use CEL expressions as part of a [policy expression pipeline]({{< ref "configure-supervisor-federationdomain-idps" >}}) +to further restrict authentication based on usernames and group names. + +For example, when you use `spec.allowAuthentication.organizations.policy: AllGitHubUsers` then any GitHub user +can authenticate. A CEL expression could be used to further restrict authentication to a set of specific users +with a policy like this: + +```yaml +transforms: + constants: + - name: allowedUsers + type: stringList + stringListValue: + - "cfryanr" + - "joshuatcasey" + expressions: + - type: policy/v1 + expression: 'username in strListConst.allowedUsers' + message: "Only certain GitHub users may authenticate" +``` + +You could also use similar CEL expressions to limit authentication by GitHub team membership. + +## Notes + +Currently, Pinniped supports GitHub API version `2022-11-28` ([ref](https://docs.github.com/en/rest/about-the-rest-api/api-versions?apiVersion=2022-11-28)). + +## Next steps + +Next, [configure the Concierge to validate JWTs issued by the Supervisor]({{< ref "configure-concierge-supervisor-jwt" >}})! +Then you'll be able to log into those clusters as any of the users from GitHub. diff --git a/site/content/docs/howto/supervisor/configure-supervisor-with-gitlab.md b/site/content/docs/howto/supervisor/configure-supervisor-with-gitlab.md index e1e5ad081..d0871160b 100644 --- a/site/content/docs/howto/supervisor/configure-supervisor-with-gitlab.md +++ b/site/content/docs/howto/supervisor/configure-supervisor-with-gitlab.md @@ -43,7 +43,7 @@ For example, to create a user-owned application: ## Configure the Supervisor cluster -Create an [OIDCIdentityProvider](https://github.com/vmware-tanzu/pinniped/blob/main/generated/{{< latestcodegenversion >}}/README.adoc#oidcidentityprovider) in the same namespace as the Supervisor. +Create an [OIDCIdentityProvider](https://github.com/vmware-tanzu/pinniped/blob/main/generated/latest/README.adoc#oidcidentityprovider) in the same namespace as the Supervisor. For example, this OIDCIdentityProvider and corresponding Secret for [gitlab.com](https://gitlab.com) use the `nickname` claim (GitLab username) as the Kubernetes username: diff --git a/site/content/docs/howto/supervisor/configure-supervisor-with-jumpcloudldap.md b/site/content/docs/howto/supervisor/configure-supervisor-with-jumpcloudldap.md index 313824bf6..a9b5a0c65 100644 --- a/site/content/docs/howto/supervisor/configure-supervisor-with-jumpcloudldap.md +++ b/site/content/docs/howto/supervisor/configure-supervisor-with-jumpcloudldap.md @@ -48,7 +48,7 @@ Here are some good resources to review while setting up and using JumpCloud's LD ## Configure the Supervisor cluster -Create an [LDAPIdentityProvider](https://github.com/vmware-tanzu/pinniped/blob/main/generated/{{< latestcodegenversion >}}/README.adoc#ldapidentityprovider) in the same namespace as the Supervisor. +Create an [LDAPIdentityProvider](https://github.com/vmware-tanzu/pinniped/blob/main/generated/latest/README.adoc#ldapidentityprovider) in the same namespace as the Supervisor. For example, this LDAPIdentityProvider configures the LDAP entry's `uid` as the Kubernetes username, and the `cn` (common name) of each group to which the user belongs as the Kubernetes group names. diff --git a/site/content/docs/howto/supervisor/configure-supervisor-with-okta.md b/site/content/docs/howto/supervisor/configure-supervisor-with-okta.md index 53f6cf18d..19b4a1d01 100644 --- a/site/content/docs/howto/supervisor/configure-supervisor-with-okta.md +++ b/site/content/docs/howto/supervisor/configure-supervisor-with-okta.md @@ -51,7 +51,7 @@ For example, to create an app: ## Configure the Supervisor -Create an [OIDCIdentityProvider](https://github.com/vmware-tanzu/pinniped/blob/main/generated/{{< latestcodegenversion >}}/README.adoc#oidcidentityprovider) in the same namespace as the Supervisor. +Create an [OIDCIdentityProvider](https://github.com/vmware-tanzu/pinniped/blob/main/generated/latest/README.adoc#oidcidentityprovider) in the same namespace as the Supervisor. For example, this OIDCIdentityProvider and corresponding Secret use Okta's `email` claim as the Kubernetes username: diff --git a/site/content/docs/howto/supervisor/configure-supervisor-with-openldap.md b/site/content/docs/howto/supervisor/configure-supervisor-with-openldap.md index d6e70c45c..46aff6716 100644 --- a/site/content/docs/howto/supervisor/configure-supervisor-with-openldap.md +++ b/site/content/docs/howto/supervisor/configure-supervisor-with-openldap.md @@ -188,7 +188,7 @@ kubectl apply -f openldap.yaml ## Configure the Supervisor cluster -Create an [LDAPIdentityProvider](https://github.com/vmware-tanzu/pinniped/blob/main/generated/{{< latestcodegenversion >}}/README.adoc#ldapidentityprovider) in the same namespace as the Supervisor. +Create an [LDAPIdentityProvider](https://github.com/vmware-tanzu/pinniped/blob/main/generated/latest/README.adoc#ldapidentityprovider) in the same namespace as the Supervisor. For example, this LDAPIdentityProvider configures the LDAP entry's `uid` as the Kubernetes username, and the `cn` (common name) of each group to which the user belongs as the Kubernetes group names. diff --git a/site/content/docs/howto/supervisor/configure-supervisor-with-workspace_one_access.md b/site/content/docs/howto/supervisor/configure-supervisor-with-workspace_one_access.md index 7b7455295..0a04e6111 100644 --- a/site/content/docs/howto/supervisor/configure-supervisor-with-workspace_one_access.md +++ b/site/content/docs/howto/supervisor/configure-supervisor-with-workspace_one_access.md @@ -54,7 +54,7 @@ For example, to create an app: ## Configure the Supervisor -Create an [OIDCIdentityProvider](https://github.com/vmware-tanzu/pinniped/blob/main/generated/{{< latestcodegenversion >}}/README.adoc#oidcidentityprovider) in the same namespace as the Supervisor. +Create an [OIDCIdentityProvider](https://github.com/vmware-tanzu/pinniped/blob/main/generated/latest/README.adoc#oidcidentityprovider) in the same namespace as the Supervisor. For example, this OIDCIdentityProvider and corresponding Secret use Workspace ONE Access's `email` claim as the Kubernetes username: diff --git a/site/content/docs/howto/supervisor/configure-supervisor.md b/site/content/docs/howto/supervisor/configure-supervisor.md index 15adbfe3c..bdb044abb 100644 --- a/site/content/docs/howto/supervisor/configure-supervisor.md +++ b/site/content/docs/howto/supervisor/configure-supervisor.md @@ -329,7 +329,7 @@ should be signed by a certificate authority that is trusted by their browsers. ## Next steps -Next, configure an OIDCIdentityProvider, ActiveDirectoryIdentityProvider, or an LDAPIdentityProvider for the Supervisor +Next, configure an OIDCIdentityProvider, ActiveDirectoryIdentityProvider, LDAPIdentityProvider, or a GitHubIdentityProvider for the Supervisor (several examples are available in these guides). Then learn [how to configure a FederationDomain to use one or more identity providers]({{< ref "configure-supervisor-federationdomain-idps" >}}). And finally, [configure the Concierge to use the Supervisor for authentication]({{< ref "configure-concierge-supervisor-jwt" >}}) diff --git a/site/content/docs/reference/active-directory-configuration.md b/site/content/docs/reference/active-directory-configuration.md index 683fce93e..f7e411a1b 100644 --- a/site/content/docs/reference/active-directory-configuration.md +++ b/site/content/docs/reference/active-directory-configuration.md @@ -11,7 +11,7 @@ menu: --- This describes the default values for the `ActiveDirectoryIdentityProvider` user and group search. For more about `ActiveDirectoryIdentityProvider` -configuration, see [the API reference documentation](https://github.com/vmware-tanzu/pinniped/blob/main/generated/{{< latestcodegenversion >}}/README.adoc#activedirectoryidentityprovider). +configuration, see [the API reference documentation](https://github.com/vmware-tanzu/pinniped/blob/main/generated/latest/README.adoc#activedirectoryidentityprovider). ### `spec.userSearch.base` diff --git a/site/content/docs/reference/api.md b/site/content/docs/reference/api.md index 34b642985..c115dee57 100644 --- a/site/content/docs/reference/api.md +++ b/site/content/docs/reference/api.md @@ -9,4 +9,4 @@ menu: weight: 35 parent: reference --- -Full API reference documentation for the Pinniped Kubernetes API is available [on GitHub](https://github.com/vmware-tanzu/pinniped/blob/main/generated/{{< latestcodegenversion >}}/README.adoc). +Full API reference documentation for the Pinniped Kubernetes API is available [on GitHub](https://github.com/vmware-tanzu/pinniped/blob/main/generated/latest/README.adoc). diff --git a/site/content/docs/reference/cli.md b/site/content/docs/reference/cli.md index 42e491034..ddf9e45e7 100644 --- a/site/content/docs/reference/cli.md +++ b/site/content/docs/reference/cli.md @@ -205,7 +205,7 @@ pinniped get kubeconfig [flags] --timeout duration Timeout for autodiscovery and validation (default 10m0s) --upstream-identity-provider-flow string The type of client flow to use with the upstream identity provider during login with a Supervisor (e.g. 'cli_password', 'browser_authcode') --upstream-identity-provider-name string The name of the upstream identity provider used during login with a Supervisor - --upstream-identity-provider-type string The type of the upstream identity provider used during login with a Supervisor (e.g. 'oidc', 'ldap', 'activedirectory') + --upstream-identity-provider-type string The type of the upstream identity provider used during login with a Supervisor (e.g. 'oidc', 'ldap', 'activedirectory', 'github') ``` ### SEE ALSO @@ -277,7 +277,7 @@ pinniped login oidc --issuer ISSUER [flags] --skip-browser Skip opening the browser (just print the URL) --upstream-identity-provider-flow string The type of client flow to use with the upstream identity provider during login with a Supervisor (e.g. 'browser_authcode', 'cli_password') --upstream-identity-provider-name string The name of the upstream identity provider used during login with a Supervisor - --upstream-identity-provider-type string The type of the upstream identity provider used during login with a Supervisor (e.g. 'oidc', 'ldap', 'activedirectory') (default "oidc") + --upstream-identity-provider-type string The type of the upstream identity provider used during login with a Supervisor (e.g. 'oidc', 'ldap', 'activedirectory', 'github') (default "oidc") ``` ### SEE ALSO diff --git a/site/content/docs/reference/code-walkthrough.md b/site/content/docs/reference/code-walkthrough.md index bd18b6f91..3218dfbaf 100644 --- a/site/content/docs/reference/code-walkthrough.md +++ b/site/content/docs/reference/code-walkthrough.md @@ -208,7 +208,7 @@ The per-FederationDomain endpoints are: extended in [internal/federationdomain/endpoints/tokenexchange/token_exchange.go](https://github.com/vmware-tanzu/pinniped/blob/main/internal/federationdomain/endpoints/tokenexchange/token_exchange.go) to handle an additional grant type for [RFC 8693](https://datatracker.ietf.org/doc/html/rfc8693) token exchanges to reduce the applicable scope (technically, the `aud` claim) of ID tokens. -- `/callback` is a special endpoint that is used as the redirect URL when performing an OIDC authcode flow against an upstream OIDC identity provider as configured by an OIDCIdentityProvider custom resource. +- `/callback` is a special endpoint that is used as the redirect URL when performing an OAuth 2.0 or OIDC authcode flow against an upstream OIDC identity provider as configured by an OIDCIdentityProvider or GitHubIdentityProvider custom resource. See [internal/federationdomain/endpoints/callback/callback_handler.go](https://github.com/vmware-tanzu/pinniped/blob/main/internal/federationdomain/endpoints/callback/callback_handler.go). - `/v1alpha1/pinniped_identity_providers` is a custom discovery endpoint for clients to learn about available upstream identity providers. See [internal/federationdomain/endpoints/idpdiscovery/idp_discovery_handler.go](https://github.com/vmware-tanzu/pinniped/blob/main/internal/federationdomain/endpoints/idpdiscovery/idp_discovery_handler.go). diff --git a/site/content/docs/reference/supported-clusters.md b/site/content/docs/reference/supported-clusters.md index 1eff4385d..9c55c287b 100644 --- a/site/content/docs/reference/supported-clusters.md +++ b/site/content/docs/reference/supported-clusters.md @@ -30,7 +30,7 @@ Most managed Kubernetes services do not support this. 2. Impersonation Proxy: Can be run on any Kubernetes cluster. Default configuration requires that a `LoadBalancer` service can be created. Most cloud-hosted Kubernetes environments have this capability. The Impersonation Proxy automatically provisions (when `spec.impersonationProxy.mode` is set to `auto`) a `LoadBalancer` for ingress to the impersonation endpoint. Users who wish to use the impersonation proxy without an automatically configured `LoadBalancer` can do so with an automatically provisioned `ClusterIP` or with a Service that they provision themselves. These options -can be configured in the spec of the [`CredentialIssuer`](https://github.com/vmware-tanzu/pinniped/blob/main/generated/{{< latestcodegenversion >}}/README.adoc#credentialissuer). +can be configured in the spec of the [`CredentialIssuer`](https://github.com/vmware-tanzu/pinniped/blob/main/generated/latest/README.adoc#credentialissuer). If a cluster is capable of supporting both strategies, the Pinniped CLI will use the token credential request API strategy by default. diff --git a/site/content/docs/tutorials/concierge-and-supervisor-demo.md b/site/content/docs/tutorials/concierge-and-supervisor-demo.md index 6f0493789..e2056216e 100644 --- a/site/content/docs/tutorials/concierge-and-supervisor-demo.md +++ b/site/content/docs/tutorials/concierge-and-supervisor-demo.md @@ -361,7 +361,7 @@ kubectl get secret supervisor-tls-cert \ ### Configure a FederationDomain in the Pinniped Supervisor -The Supervisor should be configured to have a [FederationDomain](https://github.com/vmware-tanzu/pinniped/blob/main/generated/{{< latestcodegenversion >}}/README.adoc#federationdomain), which, under the hood: +The Supervisor should be configured to have a [FederationDomain](https://github.com/vmware-tanzu/pinniped/blob/main/generated/latest/README.adoc#federationdomain), which, under the hood: - Acts as an OIDC provider to the Pinniped CLI, creating a consistent interface for the CLI to use regardless of which protocol the Supervisor is using to talk to the external identity provider - Also acts as an OIDC provider to the workload cluster's Concierge component, which will receive JWT tokens @@ -417,7 +417,7 @@ The general steps required to create and configure a client in Okta are: ### Configure the Supervisor to use Okta as the external identity provider -Create an [OIDCIdentityProvider](https://github.com/vmware-tanzu/pinniped/blob/main/generated/{{< latestcodegenversion >}}/README.adoc#oidcidentityprovider) and a Secret. +Create an [OIDCIdentityProvider](https://github.com/vmware-tanzu/pinniped/blob/main/generated/latest/README.adoc#oidcidentityprovider) and a Secret. ```sh # Replace the issuer's domain, the client ID, and client secret below. @@ -488,7 +488,7 @@ kubectl apply -f \ Configure the Concierge on the first workload cluster to trust the Supervisor's FederationDomain for authentication by creating a -[JWTAuthenticator](https://github.com/vmware-tanzu/pinniped/blob/main/generated/{{< latestcodegenversion >}}/README.adoc#jwtauthenticator). +[JWTAuthenticator](https://github.com/vmware-tanzu/pinniped/blob/main/generated/latest/README.adoc#jwtauthenticator). ```sh # The audience value below is an arbitrary value which must uniquely diff --git a/site/content/docs/tutorials/supervisor-without-concierge-demo.md b/site/content/docs/tutorials/supervisor-without-concierge-demo.md index fbf4996e6..ce83126bc 100644 --- a/site/content/docs/tutorials/supervisor-without-concierge-demo.md +++ b/site/content/docs/tutorials/supervisor-without-concierge-demo.md @@ -51,7 +51,7 @@ clusters all with a single Pinniped Supervisor. 1. A Pinniped Supervisor already installed and running on another cluster, and already configured with a working FederationDomain, TLS certificates, and an external identity provider - (e.g. an OIDCIdentityProvider, LDAPIdentityProvider, or ActiveDirectoryIdentityProvider). + (e.g. an OIDCIdentityProvider, LDAPIdentityProvider, ActiveDirectoryIdentityProvider, or GitHubIdentityProvider). Don't have a Pinniped Supervisor ready? Please refer to the other documents on this site to help you get one up and running and sufficiently configured. diff --git a/site/content/posts/2023-09-19-multiple-idps-and-identity-transformations.md b/site/content/posts/2023-09-19-multiple-idps-and-identity-transformations.md index e8a384d56..93c717b2d 100644 --- a/site/content/posts/2023-09-19-multiple-idps-and-identity-transformations.md +++ b/site/content/posts/2023-09-19-multiple-idps-and-identity-transformations.md @@ -132,7 +132,7 @@ with these new features, see: - The documentation for [creating FederationDomains]({{< ref "docs/howto/supervisor/configure-supervisor.md" >}}). - The documentation for [configuring identity providers on FederationDomains]({{< ref "docs/howto/supervisor/configure-supervisor-federationdomain-idps.md" >}}). - The API documentation for the `spec.identityProviders` setting on the -[FederationDomain](https://github.com/vmware-tanzu/pinniped/blob/main/generated/{{< latestcodegenversion >}}/README.adoc#federationdomain) +[FederationDomain](https://github.com/vmware-tanzu/pinniped/blob/main/generated/latest/README.adoc#federationdomain) resource. {{< community >}} diff --git a/site/content/posts/2024-06-06-githubidentityprovider.md b/site/content/posts/2024-06-06-githubidentityprovider.md new file mode 100644 index 000000000..cada2c775 --- /dev/null +++ b/site/content/posts/2024-06-06-githubidentityprovider.md @@ -0,0 +1,193 @@ +--- +title: "Pinniped v0.31.0: GitHub as an identity provider" +slug: github-idp-support +date: 2024-06-06 +author: Ryan Richard +image: https://images.unsplash.com/photo-1557657043-23eec69b89c9?q=80&w=3008&auto=format&fit=crop&ixlib=rb-4.0.3 +excerpt: "With the release of v0.31.0, Pinniped brings GitHub identities to Kubernetes clusters everywhere" +tags: ['Ryan Richard', 'release'] +--- + +![sunbathing seal](https://images.unsplash.com/photo-1557657043-23eec69b89c9?q=80&w=3008&auto=format&fit=crop&ixlib=rb-4.0.3) +*Photo from [Unsplash](https://unsplash.com/photos/white-seal-on-soil-giZJHm2m9yY)* + +Pinniped's v0.31.0 release brings your enterprise's developer and operator GitHub identities +to all your Kubernetes clusters. +Previously, Pinniped supported external identity providers of types +OpenID Connect (OIDC), Lightweight Directory Access Protocol (LDAP), and Active +Directory (AD) configured for either one or many clusters. +If you're already managing your source code on github.com or using GitHub Enterprise, +then your developers and operators already have GitHub identities. +Now you can easily control their authentication and authorization to your fleets of Kubernetes clusters +using that same GitHub identity, with the same great security and user experience that Pinniped already offers. + +Additionally, the release includes several dependency updates and other changes. +See the [release notes](https://github.com/vmware-tanzu/pinniped/releases/tag/v0.31.0) for more details. + +## Configuring GitHub authentication + +Using GitHub is as easy as creating a GitHubIdentityProvider resource in your Supervisor's namespace, and then +adding it to your FederationDomain resource's spec.identityProviders. Once configured, then you can generate +kubeconfigs for your clusters and hand those out to your end-users. As always with the Pinniped Supervisor, +these kubeconfig files will not contain any particular identity or credentials, and can be shared among +all users of that cluster. + +## The minimum configuration + +Here is the most basic example of a GitHubIdentityProvider. +You'll need to configure a new GitHub App or GitHub OAuth app on GitHub +and note the client ID and client secret for use in your Pinniped configuration. +See the [GitHub configuration guide]({{< ref "docs/howto/supervisor/configure-supervisor-with-github.md" >}}) +for details about how to create a GitHub App or GitHub OAuth app. + +```yaml +apiVersion: idp.supervisor.pinniped.dev/v1alpha1 +kind: GitHubIdentityProvider +metadata: + name: my-github-provider + namespace: pinniped-supervisor +spec: + allowAuthentication: + organizations: + policy: AllGitHubUsers + client: + secretName: my-github-provider-client-secret +--- +apiVersion: v1 +kind: Secret +type: "secrets.pinniped.dev/github-client" +metadata: + name: my-github-provider-client-secret + namespace: pinniped-supervisor +stringData: + clientID: + clientSecret: +``` + +This GitHubIdentityProvider uses github.com (the default) and allows any user of github.com to authenticate. + +But wait a minute! Any user of github.com? Aren't there millions of users? Yes, there are. +This simplest configuration example is great for a demo or for a Kubernetes cluster running on your laptop, +but you may not want to use this for your enterprise's fleets of Kubernetes clusters. + +However, note that you could use the above GitHubIdentityProvider along with a policy on the FederationDomain +to reject authentication for any user unless they belong to certain GitHub teams. For example: + +```yaml +apiVersion: config.supervisor.pinniped.dev/v1alpha1 +kind: FederationDomain +metadata: + name: my-federation-domain + namespace: pinniped-supervisor +spec: + issuer: https://pinniped.example.com/my-issuer-path + identityProviders: + - displayName: "My GitHub IDP 🚀" + objectRef: + apiGroup: idp.supervisor.pinniped.dev + kind: GitHubIdentityProvider + name: my-github-provider + transforms: + expressions: + - type: policy/v1 + expression: 'groups.exists(g, g in ["my-github-org/team1", "my-github-org/team2"])' + message: "Only users in certain GitHub teams are allowed to authenticate" +``` + +Now users must belong to one of the two teams configured above to be able to successfully authenticate. + +## Restricting authentication by GitHub organization membership + +Would you rather only allow members of certain GitHub organizations to authenticate? No problem, we've got you covered. +Just make a small change to your GitHubIdentityProvider. + +```yaml +apiVersion: idp.supervisor.pinniped.dev/v1alpha1 +kind: GitHubIdentityProvider +metadata: + name: my-github-provider + namespace: pinniped-supervisor +spec: + allowAuthentication: + organizations: + policy: OnlyUsersFromAllowedOrganizations + allowed: + - my-enterprise-organization # this is case-insensitive + client: + secretName: my-github-provider-client-secret +``` + +Now users must belong to the organization configured above to successfully authenticate. + +When multiple orgs are `allowed` then the user must belong to any one of those orgs. + +Want to further restrict auth by GitHub team membership? +No problem, you can still create a `policy/v1` expression as shown in the previous example above. + +## Using GitHub Enterprise + +Are you running GitHub Enterprise for your source control needs? You can use your GitHub Enterprise server's user +identities by specifying the `host` and optional `tls.certificateAuthorityData`. + +```yaml +apiVersion: idp.supervisor.pinniped.dev/v1alpha1 +kind: GitHubIdentityProvider +metadata: + name: my-github-provider + namespace: pinniped-supervisor +spec: + host: github.my-enterprise.example.com + tls: + certificateAuthorityData: LS0tLS1CRUdJTiBDRVJUSUZJQ0FU.... # optional + allowAuthentication: + organizations: + policy: OnlyUsersFromAllowedOrganizations + allowed: + - my-enterprise-organization + client: + secretName: my-github-provider-client-secret +``` + +## Kubernetes username and group names + +The GitHubIdentityProvider resource offers several choices for how your users' Kubernetes usernames and group names should look. + +`spec.claims.username` allows you to choose from: +- `login`: The user's GitHub username as shown on their profile. Note that a user can change their own username, + so this is not recommended for production use with identities from public github.com. +- `id`: The numeric user ID assigned by GitHub will be used as the username. On public github.com, this can be found for any user + by putting their login name into this GitHub API URL: `https://api.github.com/users/cfryanr` (replace `cfryanr` with the login name). + This is automatically assigned and immutable for each user. +- `login:id`: Blends the readability of using login names with the immutability of using IDs by putting both into + the Kubernetes usernames, separated by a colon. This keeps your RBAC policies nicely readable. This is the default. + +`spec.claims.groups` allows you to choose from: +- `name`: GitHub team names can include mixed-case characters, spaces, and punctuation, e.g. `Kube admins!`. +- `slug`: GitHub slug names are lower-cased, with spaces replaced by hyphens, and other punctuation removed, e.g. `kube-admins`. + This is the default. +- Either way, the team names will automatically be prefixed by the name of the org in which the team resides, with a `/` separator, + e.g. `My-org/kube-admins`. The org name will preserve its case from GitHub. + +## Control new and existing sessions by changing org and team memberships on GitHub + +Did one of your developers or operators just change teams or leave your enterprise? Fear not. Simply update their +GitHub organization and/or GitHub team memberships on github.com and Pinniped will respect those changes almost immediately. + +When one of your end users starts a new session, your org and team-based restrictions will apply using your +updated org and team memberships immediately. + +For your end-users with pre-existing ongoing sessions Pinniped will see the new org and team memberships at the next +session refresh, which happens approximately every 5 minutes in a standard Pinniped configuration for active sessions. +Your Kubernetes RBAC policies will see the updated group memberships after the next refresh. +There is no way for your end users to avoid these refreshes without losing access to your clusters. + +## Where to read more + +This blog post is just a quick overview of this new feature. To learn about how to configure the Pinniped Supervisor +with this new feature, see: + +- The [GitHub configuration guide]({{< ref "docs/howto/supervisor/configure-supervisor-with-github.md" >}}). +- The [GitHubIdentityProvider resource](https://github.com/vmware-tanzu/pinniped/blob/main/generated/latest/README.adoc#githubidentityprovider) documentation. +- The documentation for [configuring identity providers on FederationDomains]({{< ref "docs/howto/supervisor/configure-supervisor-federationdomain-idps.md" >}}). + +{{< community >}} diff --git a/test/integration/e2e_test.go b/test/integration/e2e_test.go index 2f853f902..4175f69fc 100644 --- a/test/integration/e2e_test.go +++ b/test/integration/e2e_test.go @@ -160,7 +160,7 @@ func TestE2EFullIntegration_Browser(t *testing.T) { Groups: env.SupervisorUpstreamOIDC.GroupsClaim, }, Client: idpv1alpha1.OIDCClient{ - SecretName: testlib.CreateClientCredsSecret(t, env.SupervisorUpstreamOIDC.ClientID, env.SupervisorUpstreamOIDC.ClientSecret).Name, + SecretName: testlib.CreateOIDCClientCredentialsSecret(t, env.SupervisorUpstreamOIDC.ClientID, env.SupervisorUpstreamOIDC.ClientSecret).Name, }, }, idpv1alpha1.PhaseReady) testlib.WaitForFederationDomainStatusPhase(testCtx, t, federationDomain.Name, supervisorconfigv1alpha1.FederationDomainPhaseReady) @@ -246,7 +246,7 @@ func TestE2EFullIntegration_Browser(t *testing.T) { Groups: env.SupervisorUpstreamOIDC.GroupsClaim, }, Client: idpv1alpha1.OIDCClient{ - SecretName: testlib.CreateClientCredsSecret(t, env.SupervisorUpstreamOIDC.ClientID, env.SupervisorUpstreamOIDC.ClientSecret).Name, + SecretName: testlib.CreateOIDCClientCredentialsSecret(t, env.SupervisorUpstreamOIDC.ClientID, env.SupervisorUpstreamOIDC.ClientSecret).Name, }, }, idpv1alpha1.PhaseReady) testlib.WaitForFederationDomainStatusPhase(testCtx, t, federationDomain.Name, supervisorconfigv1alpha1.FederationDomainPhaseReady) @@ -334,7 +334,7 @@ func TestE2EFullIntegration_Browser(t *testing.T) { Groups: env.SupervisorUpstreamOIDC.GroupsClaim, }, Client: idpv1alpha1.OIDCClient{ - SecretName: testlib.CreateClientCredsSecret(t, env.SupervisorUpstreamOIDC.ClientID, env.SupervisorUpstreamOIDC.ClientSecret).Name, + SecretName: testlib.CreateOIDCClientCredentialsSecret(t, env.SupervisorUpstreamOIDC.ClientID, env.SupervisorUpstreamOIDC.ClientSecret).Name, }, }, idpv1alpha1.PhaseReady) testlib.WaitForFederationDomainStatusPhase(testCtx, t, federationDomain.Name, supervisorconfigv1alpha1.FederationDomainPhaseReady) @@ -458,7 +458,7 @@ func TestE2EFullIntegration_Browser(t *testing.T) { Groups: env.SupervisorUpstreamOIDC.GroupsClaim, }, Client: idpv1alpha1.OIDCClient{ - SecretName: testlib.CreateClientCredsSecret(t, env.SupervisorUpstreamOIDC.ClientID, env.SupervisorUpstreamOIDC.ClientSecret).Name, + SecretName: testlib.CreateOIDCClientCredentialsSecret(t, env.SupervisorUpstreamOIDC.ClientID, env.SupervisorUpstreamOIDC.ClientSecret).Name, }, }, idpv1alpha1.PhaseReady) testlib.WaitForFederationDomainStatusPhase(testCtx, t, federationDomain.Name, supervisorconfigv1alpha1.FederationDomainPhaseReady) @@ -589,7 +589,7 @@ func TestE2EFullIntegration_Browser(t *testing.T) { Groups: env.SupervisorUpstreamOIDC.GroupsClaim, }, Client: idpv1alpha1.OIDCClient{ - SecretName: testlib.CreateClientCredsSecret(t, env.SupervisorUpstreamOIDC.ClientID, env.SupervisorUpstreamOIDC.ClientSecret).Name, + SecretName: testlib.CreateOIDCClientCredentialsSecret(t, env.SupervisorUpstreamOIDC.ClientID, env.SupervisorUpstreamOIDC.ClientSecret).Name, }, }, idpv1alpha1.PhaseReady) testlib.WaitForFederationDomainStatusPhase(testCtx, t, federationDomain.Name, supervisorconfigv1alpha1.FederationDomainPhaseReady) @@ -662,7 +662,7 @@ func TestE2EFullIntegration_Browser(t *testing.T) { Groups: env.SupervisorUpstreamOIDC.GroupsClaim, }, Client: idpv1alpha1.OIDCClient{ - SecretName: testlib.CreateClientCredsSecret(t, env.SupervisorUpstreamOIDC.ClientID, env.SupervisorUpstreamOIDC.ClientSecret).Name, + SecretName: testlib.CreateOIDCClientCredentialsSecret(t, env.SupervisorUpstreamOIDC.ClientID, env.SupervisorUpstreamOIDC.ClientSecret).Name, }, }, idpv1alpha1.PhaseReady) testlib.WaitForFederationDomainStatusPhase(testCtx, t, federationDomain.Name, supervisorconfigv1alpha1.FederationDomainPhaseReady) @@ -1209,6 +1209,93 @@ func TestE2EFullIntegration_Browser(t *testing.T) { sessionCachePath, pinnipedExe, expectedUsername, expectedGroups, allScopes) }) + t.Run("with Supervisor GitHub upstream IDP and browser flow with with form_post automatic authcode delivery to CLI", func(t *testing.T) { + testlib.SkipTestWhenGitHubIsUnavailable(t) + + testCtx, cancel := context.WithTimeout(context.Background(), 5*time.Minute) + t.Cleanup(cancel) + + tempDir := t.TempDir() // per-test tmp dir to avoid sharing files between tests + + // Start a fresh browser driver because we don't want to share cookies between the various tests in this file. + browser := browsertest.OpenBrowser(t) + + expectedUsername := env.SupervisorUpstreamGithub.TestUserUsername + ":" + env.SupervisorUpstreamGithub.TestUserID + expectedGroups := env.SupervisorUpstreamGithub.TestUserExpectedTeamSlugs + + // Create a ClusterRoleBinding to give our test user from the upstream read-only access to the cluster. + testlib.CreateTestClusterRoleBinding(t, + rbacv1.Subject{Kind: rbacv1.UserKind, APIGroup: rbacv1.GroupName, Name: expectedUsername}, + rbacv1.RoleRef{Kind: "ClusterRole", APIGroup: rbacv1.GroupName, Name: "view"}, + ) + testlib.WaitForUserToHaveAccess(t, expectedUsername, []string{}, &authorizationv1.ResourceAttributes{ + Verb: "get", + Group: "", + Version: "v1", + Resource: "namespaces", + }) + + // Create upstream GitHub provider and wait for it to become ready. + createdProvider := testlib.CreateTestGitHubIdentityProvider(t, idpv1alpha1.GitHubIdentityProviderSpec{ + AllowAuthentication: idpv1alpha1.GitHubAllowAuthenticationSpec{ + Organizations: idpv1alpha1.GitHubOrganizationsSpec{ + Policy: ptr.To(idpv1alpha1.GitHubAllowedAuthOrganizationsPolicyAllGitHubUsers), + }, + }, + Claims: idpv1alpha1.GitHubClaims{ + Username: ptr.To(idpv1alpha1.GitHubUsernameLoginAndID), + Groups: ptr.To(idpv1alpha1.GitHubUseTeamSlugForGroupName), + }, + Client: idpv1alpha1.GitHubClientSpec{ + SecretName: testlib.CreateGitHubClientCredentialsSecret(t, + env.SupervisorUpstreamGithub.GithubAppClientID, + env.SupervisorUpstreamGithub.GithubAppClientSecret, + ).Name, + }, + }, idpv1alpha1.GitHubPhaseReady) + testlib.WaitForFederationDomainStatusPhase(testCtx, t, federationDomain.Name, configv1alpha1.FederationDomainPhaseReady) + testlib.WaitForJWTAuthenticatorStatusPhase(testCtx, t, authenticator.Name, authv1alpha.JWTAuthenticatorPhaseReady) + + // Use a specific session cache for this test. + sessionCachePath := tempDir + "/test-sessions.yaml" + credentialCachePath := tempDir + "/test-credentials.yaml" + + kubeconfigPath := runPinnipedGetKubeconfig(t, env, pinnipedExe, tempDir, []string{ + "get", "kubeconfig", + "--concierge-api-group-suffix", env.APIGroupSuffix, + "--concierge-authenticator-type", "jwt", + "--concierge-authenticator-name", authenticator.Name, + "--oidc-skip-browser", + "--oidc-ca-bundle", testCABundlePath, + "--oidc-session-cache", sessionCachePath, + "--credential-cache", credentialCachePath, + // use default for --oidc-scopes, which is to request all relevant scopes + }) + + // Run "kubectl get namespaces" which should trigger a browser login via the plugin. + kubectlCmd := exec.CommandContext(testCtx, "kubectl", "get", "namespace", "--kubeconfig", kubeconfigPath, "-v", "6") + kubectlCmd.Env = append(os.Environ(), env.ProxyEnv()...) + + // Run the kubectl command, wait for the Pinniped CLI to print the authorization URL, and open it in the browser. + kubectlOutputChan := startKubectlAndOpenAuthorizationURLInBrowser(testCtx, t, kubectlCmd, browser) + + // Confirm that we got to the upstream IDP's login page, fill out the form, and submit the form. + browsertest.LoginToUpstreamGitHub(t, browser, env.SupervisorUpstreamGithub) + + // Expect to be redirected to the downstream callback which is serving the form_post HTML. + t.Logf("waiting for response page %s", federationDomain.Spec.Issuer) + browser.WaitForURL(t, regexp.MustCompile(regexp.QuoteMeta(federationDomain.Spec.Issuer))) + + // The response page should have done the background fetch() and POST'ed to the CLI's callback. + // It should now be in the "success" state. + formpostExpectSuccessState(t, browser) + + requireKubectlGetNamespaceOutput(t, env, waitForKubectlOutput(t, kubectlOutputChan)) + + requireUserCanUseKubectlWithoutAuthenticatingAgain(testCtx, t, env, federationDomain, createdProvider.Name, kubeconfigPath, + sessionCachePath, pinnipedExe, expectedUsername, expectedGroups, allScopes) + }) + t.Run("with multiple IDPs: one OIDC and one LDAP", func(t *testing.T) { testlib.SkipTestWhenLDAPIsUnavailable(t, env) @@ -1270,7 +1357,7 @@ func TestE2EFullIntegration_Browser(t *testing.T) { Groups: env.SupervisorUpstreamOIDC.GroupsClaim, }, Client: idpv1alpha1.OIDCClient{ - SecretName: testlib.CreateClientCredsSecret(t, env.SupervisorUpstreamOIDC.ClientID, env.SupervisorUpstreamOIDC.ClientSecret).Name, + SecretName: testlib.CreateOIDCClientCredentialsSecret(t, env.SupervisorUpstreamOIDC.ClientID, env.SupervisorUpstreamOIDC.ClientSecret).Name, }, }, idpv1alpha1.PhaseReady) @@ -1591,7 +1678,7 @@ func TestE2EFullIntegration_Browser(t *testing.T) { Groups: env.SupervisorUpstreamOIDC.GroupsClaim, }, Client: idpv1alpha1.OIDCClient{ - SecretName: testlib.CreateClientCredsSecret(t, env.SupervisorUpstreamOIDC.ClientID, env.SupervisorUpstreamOIDC.ClientSecret).Name, + SecretName: testlib.CreateOIDCClientCredentialsSecret(t, env.SupervisorUpstreamOIDC.ClientID, env.SupervisorUpstreamOIDC.ClientSecret).Name, }, }, idpv1alpha1.PhaseReady) diff --git a/test/integration/kube_api_discovery_test.go b/test/integration/kube_api_discovery_test.go index 5136f1aff..571adbdfe 100644 --- a/test/integration/kube_api_discovery_test.go +++ b/test/integration/kube_api_discovery_test.go @@ -248,6 +248,20 @@ func TestGetAPIResourceList(t *testing.T) { //nolint:gocyclo // each t.Run is pr Kind: "ActiveDirectoryIdentityProvider", Verbs: []string{"get", "patch", "update"}, }, + { + Name: "githubidentityproviders", + SingularName: "githubidentityprovider", + Namespaced: true, + Kind: "GitHubIdentityProvider", + Verbs: []string{"delete", "deletecollection", "get", "list", "patch", "create", "update", "watch"}, + Categories: []string{"pinniped", "pinniped-idp", "pinniped-idps"}, + }, + { + Name: "githubidentityproviders/status", + Namespaced: true, + Kind: "GitHubIdentityProvider", + Verbs: []string{"get", "patch", "update"}, + }, }, }, }, @@ -438,7 +452,7 @@ func TestGetAPIResourceList(t *testing.T) { //nolint:gocyclo // each t.Run is pr } // manually update this value whenever you add additional fields to an API resource and then run the generator - totalExpectedAPIFields := 263 + totalExpectedAPIFields := 289 // Because we are parsing text from `kubectl explain` and because the format of that text can change // over time, make a rudimentary assertion that this test exercised the whole tree of all fields of all @@ -578,6 +592,13 @@ func TestCRDAdditionalPrinterColumns_Parallel(t *testing.T) { {Name: "Age", Type: "date", JSONPath: ".metadata.creationTimestamp"}, }, }, + addSuffix("githubidentityproviders.idp.supervisor"): { + "v1alpha1": []apiextensionsv1.CustomResourceColumnDefinition{ + {Name: "Host", Type: "string", JSONPath: ".spec.githubAPI.host"}, + {Name: "Status", Type: "string", JSONPath: ".status.phase"}, + {Name: "Age", Type: "date", JSONPath: ".metadata.creationTimestamp"}, + }, + }, addSuffix("oidcclients.config.supervisor"): { "v1alpha1": []apiextensionsv1.CustomResourceColumnDefinition{ {Name: "Privileged Scopes", Type: "string", JSONPath: `.spec.allowedScopes[?(@ == "pinniped:request-audience")]`}, @@ -588,8 +609,20 @@ func TestCRDAdditionalPrinterColumns_Parallel(t *testing.T) { }, } - actualPinnipedCRDCount := 0 - expectedPinnipedCRDCount := 8 // the current number of CRDs that we ship as part of Pinniped + // the current CRDs that we ship as part of Pinniped + expectedPinnipedCRDNames := []string{ + "activedirectoryidentityproviders.idp.supervisor." + env.APIGroupSuffix, + "credentialissuers.config.concierge." + env.APIGroupSuffix, + "federationdomains.config.supervisor." + env.APIGroupSuffix, + "githubidentityproviders.idp.supervisor." + env.APIGroupSuffix, + "jwtauthenticators.authentication.concierge." + env.APIGroupSuffix, + "ldapidentityproviders.idp.supervisor." + env.APIGroupSuffix, + "oidcclients.config.supervisor." + env.APIGroupSuffix, + "oidcidentityproviders.idp.supervisor." + env.APIGroupSuffix, + "webhookauthenticators.authentication.concierge." + env.APIGroupSuffix, + } + + actualPinnipedCRDNames := make([]string, 0) for _, crd := range crdList.Items { if !strings.Contains(crd.Spec.Group, env.APIGroupSuffix) { @@ -597,7 +630,7 @@ func TestCRDAdditionalPrinterColumns_Parallel(t *testing.T) { } // Found a Pinniped CRD, so let's check it for AdditionalPrinterColumns. - actualPinnipedCRDCount++ + actualPinnipedCRDNames = append(actualPinnipedCRDNames, crd.Name) for _, version := range crd.Spec.Versions { expectedColumns, ok := expectedColumnsPerCRDVersion[crd.Name][version.Name] @@ -611,7 +644,7 @@ func TestCRDAdditionalPrinterColumns_Parallel(t *testing.T) { } // Make sure that the logic of this test did not accidentally skip a CRD that it should have interrogated. - require.Equal(t, expectedPinnipedCRDCount, actualPinnipedCRDCount, + require.ElementsMatch(t, expectedPinnipedCRDNames, actualPinnipedCRDNames, "did not find expected number of Pinniped CRDs to check for additionalPrinterColumns") } diff --git a/test/integration/supervisor_discovery_test.go b/test/integration/supervisor_discovery_test.go index 0e2c5e864..5c0f19db0 100644 --- a/test/integration/supervisor_discovery_test.go +++ b/test/integration/supervisor_discovery_test.go @@ -7,6 +7,7 @@ import ( "context" "crypto/tls" "crypto/x509" + "encoding/base64" "encoding/json" "fmt" "io" @@ -24,6 +25,7 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/client-go/kubernetes" "k8s.io/client-go/util/retry" + "k8s.io/utils/ptr" supervisorconfigv1alpha1 "go.pinniped.dev/generated/latest/apis/supervisor/config/v1alpha1" idpv1alpha1 "go.pinniped.dev/generated/latest/apis/supervisor/idp/v1alpha1" @@ -92,7 +94,7 @@ func TestSupervisorOIDCDiscovery_Disruptive(t *testing.T) { // Test that there is no default discovery endpoint available when there are no FederationDomains. requireDiscoveryEndpointsAreNotFound(t, scheme, addr, caBundle, fmt.Sprintf("%s://%s", scheme, addr)) - // Define several unique issuer strings. Always use https in the issuer name even when we are accessing the http port. + // Define several unique issuer URLs. Always use https in the issuer URL even when we are accessing the http port. issuer1 := fmt.Sprintf("https://%s/nested/issuer1", addr) issuer2 := fmt.Sprintf("https://%s/nested/issuer2", addr) issuer3 := fmt.Sprintf("https://%s/issuer3", addr) @@ -101,7 +103,7 @@ func TestSupervisorOIDCDiscovery_Disruptive(t *testing.T) { issuer6 := fmt.Sprintf("https://%s/issuer6", addr) badIssuer := fmt.Sprintf("https://%s/badIssuer?cannot-use=queries", addr) - // When FederationDomain are created in sequence they each cause a discovery endpoint to appear only for as long as the FederationDomain exists. + // When FederationDomains are created in sequence they each cause a discovery endpoint to appear only for as long as the FederationDomain exists. config1, jwks1 := requireCreatingFederationDomainCausesDiscoveryEndpointsToAppear(ctx, t, scheme, addr, caBundle, issuer1, client) requireDeletingFederationDomainCausesDiscoveryEndpointsToDisappear(t, config1, client, ns, scheme, addr, caBundle, issuer1) config2, jwks2 := requireCreatingFederationDomainCausesDiscoveryEndpointsToAppear(ctx, t, scheme, addr, caBundle, issuer2, client) @@ -113,15 +115,15 @@ func TestSupervisorOIDCDiscovery_Disruptive(t *testing.T) { // When multiple FederationDomains exist at the same time they each serve a unique discovery endpoint. config3, jwks3 := requireCreatingFederationDomainCausesDiscoveryEndpointsToAppear(ctx, t, scheme, addr, caBundle, issuer3, client) config4, jwks4 := requireCreatingFederationDomainCausesDiscoveryEndpointsToAppear(ctx, t, scheme, addr, caBundle, issuer4, client) - requireDiscoveryEndpointsAreWorking(t, scheme, addr, caBundle, issuer3, nil) // discovery for issuer3 is still working after issuer4 started working + requireStandardDiscoveryEndpointsAreWorking(t, scheme, addr, caBundle, issuer3, nil) // discovery for issuer3 is still working after issuer4 started working // The auto-created JWK's were different from each other. require.NotEqual(t, jwks3.Keys[0]["x"], jwks4.Keys[0]["x"]) require.NotEqual(t, jwks3.Keys[0]["y"], jwks4.Keys[0]["y"]) - // Editing a provider to change the issuer name updates the endpoints that are being served. + // Editing a FederationDomain to change the issuer URL updates the endpoints that are being served. updatedConfig4 := editFederationDomainIssuerName(t, config4, client, ns, issuer5) requireDiscoveryEndpointsAreNotFound(t, scheme, addr, caBundle, issuer4) - jwks5 := requireDiscoveryEndpointsAreWorking(t, scheme, addr, caBundle, issuer5, nil) + jwks5 := requireStandardDiscoveryEndpointsAreWorking(t, scheme, addr, caBundle, issuer5, nil) // The JWK did not change when the issuer name was updated. require.Equal(t, jwks4.Keys[0], jwks5.Keys[0]) @@ -129,31 +131,37 @@ func TestSupervisorOIDCDiscovery_Disruptive(t *testing.T) { requireDeletingFederationDomainCausesDiscoveryEndpointsToDisappear(t, config3, client, ns, scheme, addr, caBundle, issuer3) requireDeletingFederationDomainCausesDiscoveryEndpointsToDisappear(t, updatedConfig4, client, ns, scheme, addr, caBundle, issuer5) - // When the same issuer is added twice, both issuers are marked as duplicates, and neither provider is serving. + // When the same issuer URL is added to two FederationDomains, both FederationDomains are marked as duplicates, and neither is serving. config6Duplicate1, _ := requireCreatingFederationDomainCausesDiscoveryEndpointsToAppear(ctx, t, scheme, addr, caBundle, issuer6, client) config6Duplicate2 := testlib.CreateTestFederationDomain(ctx, t, supervisorconfigv1alpha1.FederationDomainSpec{Issuer: issuer6}, supervisorconfigv1alpha1.FederationDomainPhaseError) requireStatus(t, client, ns, config6Duplicate1.Name, supervisorconfigv1alpha1.FederationDomainPhaseError, withFalseConditions([]string{"Ready", "IssuerIsUnique"})) requireStatus(t, client, ns, config6Duplicate2.Name, supervisorconfigv1alpha1.FederationDomainPhaseError, withFalseConditions([]string{"Ready", "IssuerIsUnique"})) requireDiscoveryEndpointsAreNotFound(t, scheme, addr, caBundle, issuer6) - // If we delete the first duplicate issuer, the second duplicate issuer starts serving. + // If we delete the first duplicate FederationDomain, the second duplicate FederationDomain starts serving. requireDelete(t, client, ns, config6Duplicate1.Name) requireWellKnownEndpointIsWorking(t, scheme, addr, caBundle, issuer6, nil) requireStatus(t, client, ns, config6Duplicate2.Name, supervisorconfigv1alpha1.FederationDomainPhaseReady, withAllSuccessfulConditions()) - // When we finally delete all issuers, the endpoint should be down. + // When we finally delete all FederationDomains, the discovery endpoints should be down. requireDeletingFederationDomainCausesDiscoveryEndpointsToDisappear(t, config6Duplicate2, client, ns, scheme, addr, caBundle, issuer6) - // "Host" headers can be used to send requests to discovery endpoints when the public address is different from the issuer name. + // "Host" headers can be used to send requests to discovery endpoints when the public address is different from the issuer URL. issuer7 := "https://some-issuer-host-and-port-that-doesnt-match-public-supervisor-address.com:2684/issuer7" config7, _ := requireCreatingFederationDomainCausesDiscoveryEndpointsToAppear(ctx, t, scheme, addr, caBundle, issuer7, client) requireDeletingFederationDomainCausesDiscoveryEndpointsToDisappear(t, config7, client, ns, scheme, addr, caBundle, issuer7) - // When we create a provider with an invalid issuer, the status is set to invalid. + // When we create a FederationDomain with an invalid issuer url, the status is set to invalid. badConfig := testlib.CreateTestFederationDomain(ctx, t, supervisorconfigv1alpha1.FederationDomainSpec{Issuer: badIssuer}, supervisorconfigv1alpha1.FederationDomainPhaseError) requireStatus(t, client, ns, badConfig.Name, supervisorconfigv1alpha1.FederationDomainPhaseError, withFalseConditions([]string{"Ready", "IssuerURLValid"})) requireDiscoveryEndpointsAreNotFound(t, scheme, addr, caBundle, badIssuer) requireDeletingFederationDomainCausesDiscoveryEndpointsToDisappear(t, badConfig, client, ns, scheme, addr, caBundle, badIssuer) + + issuer8 := fmt.Sprintf("https://%s/issuer8multipleIDP", addr) + config8 := requireIDPsListedByIDPDiscoveryEndpoint(t, env, ctx, kubeClient, ns, scheme, addr, caBundle, issuer8) + + // requireJWKSEndpointIsWorking() will give us a bit of an idea what to do... + requireDeletingFederationDomainCausesDiscoveryEndpointsToDisappear(t, config8, client, ns, scheme, addr, caBundle, issuer8) }) } } @@ -198,7 +206,7 @@ func TestSupervisorTLSTerminationWithSNI_Disruptive(t *testing.T) { ca1 := createTLSCertificateSecret(ctx, t, ns, hostname1, nil, certSecretName1, kubeClient) // Now that the Secret exists, we should be able to access the endpoints by hostname using the CA. - _ = requireDiscoveryEndpointsAreWorking(t, scheme, address, string(ca1.Bundle()), issuer1, nil) + _ = requireStandardDiscoveryEndpointsAreWorking(t, scheme, address, string(ca1.Bundle()), issuer1, nil) // Update the config to with a new .spec.tls.secretName. certSecretName1update := "integration-test-cert-1-update" @@ -219,7 +227,7 @@ func TestSupervisorTLSTerminationWithSNI_Disruptive(t *testing.T) { ca1update := createTLSCertificateSecret(ctx, t, ns, hostname1, nil, certSecretName1update, kubeClient) // Now that the Secret exists at the new name, we should be able to access the endpoints by hostname using the CA. - _ = requireDiscoveryEndpointsAreWorking(t, scheme, address, string(ca1update.Bundle()), issuer1, nil) + _ = requireStandardDiscoveryEndpointsAreWorking(t, scheme, address, string(ca1update.Bundle()), issuer1, nil) // To test SNI virtual hosting, send requests to discovery endpoints when the public address is different from the issuer name. hostname2 := "some-issuer-host-and-port-that-doesnt-match-public-supervisor-address.com" @@ -239,7 +247,7 @@ func TestSupervisorTLSTerminationWithSNI_Disruptive(t *testing.T) { ca2 := createTLSCertificateSecret(ctx, t, ns, hostname2, nil, certSecretName2, kubeClient) // Now that the Secret exists, we should be able to access the endpoints by hostname using the CA. - _ = requireDiscoveryEndpointsAreWorking(t, scheme, hostname2+":"+hostnamePort2, string(ca2.Bundle()), issuer2, map[string]string{ + _ = requireStandardDiscoveryEndpointsAreWorking(t, scheme, hostname2+":"+hostnamePort2, string(ca2.Bundle()), issuer2, map[string]string{ hostname2 + ":" + hostnamePort2: address, }) } @@ -292,7 +300,7 @@ func TestSupervisorTLSTerminationWithDefaultCerts_Disruptive(t *testing.T) { defaultCA := createTLSCertificateSecret(ctx, t, ns, "cert-hostname-doesnt-matter", []net.IP{ips[0]}, defaultTLSCertSecretName(env), kubeClient) // Now that the Secret exists, we should be able to access the endpoints by IP address using the CA. - _ = requireDiscoveryEndpointsAreWorking(t, scheme, ipWithPort, string(defaultCA.Bundle()), issuerUsingIPAddress, nil) + _ = requireStandardDiscoveryEndpointsAreWorking(t, scheme, ipWithPort, string(defaultCA.Bundle()), issuerUsingIPAddress, nil) // Create an FederationDomain with a spec.tls.secretName. certSecretName := "integration-test-cert-1" @@ -309,10 +317,10 @@ func TestSupervisorTLSTerminationWithDefaultCerts_Disruptive(t *testing.T) { // Now that the Secret exists, we should be able to access the endpoints by hostname using the CA from the SNI cert. // Hostnames are case-insensitive, so the request should still work even if the case of the hostname is different // from the case of the issuer URL's hostname. - _ = requireDiscoveryEndpointsAreWorking(t, scheme, strings.ToUpper(hostname)+":"+port, string(certCA.Bundle()), issuerUsingHostname, nil) + _ = requireStandardDiscoveryEndpointsAreWorking(t, scheme, strings.ToUpper(hostname)+":"+port, string(certCA.Bundle()), issuerUsingHostname, nil) // And we can still access the other issuer using the default cert. - _ = requireDiscoveryEndpointsAreWorking(t, scheme, ipWithPort, string(defaultCA.Bundle()), issuerUsingIPAddress, nil) + _ = requireStandardDiscoveryEndpointsAreWorking(t, scheme, ipWithPort, string(defaultCA.Bundle()), issuerUsingIPAddress, nil) } func defaultTLSCertSecretName(env *testlib.TestEnv) string { @@ -488,12 +496,12 @@ func requireCreatingFederationDomainCausesDiscoveryEndpointsToAppear( ) (*supervisorconfigv1alpha1.FederationDomain, *ExpectedJWKSResponseFormat) { t.Helper() newFederationDomain := testlib.CreateTestFederationDomain(ctx, t, supervisorconfigv1alpha1.FederationDomainSpec{Issuer: issuerName}, supervisorconfigv1alpha1.FederationDomainPhaseReady) - jwksResult := requireDiscoveryEndpointsAreWorking(t, supervisorScheme, supervisorAddress, supervisorCABundle, issuerName, nil) + jwksResult := requireStandardDiscoveryEndpointsAreWorking(t, supervisorScheme, supervisorAddress, supervisorCABundle, issuerName, nil) requireStatus(t, client, newFederationDomain.Namespace, newFederationDomain.Name, supervisorconfigv1alpha1.FederationDomainPhaseReady, withAllSuccessfulConditions()) return newFederationDomain, jwksResult } -func requireDiscoveryEndpointsAreWorking(t *testing.T, supervisorScheme, supervisorAddress, supervisorCABundle, issuerName string, dnsOverrides map[string]string) *ExpectedJWKSResponseFormat { +func requireStandardDiscoveryEndpointsAreWorking(t *testing.T, supervisorScheme, supervisorAddress, supervisorCABundle, issuerName string, dnsOverrides map[string]string) *ExpectedJWKSResponseFormat { requireWellKnownEndpointIsWorking(t, supervisorScheme, supervisorAddress, supervisorCABundle, issuerName, dnsOverrides) jwksResult := requireJWKSEndpointIsWorking(t, supervisorScheme, supervisorAddress, supervisorCABundle, issuerName, dnsOverrides) return jwksResult @@ -737,3 +745,192 @@ func newHTTPClient(t *testing.T, caBundle string, dnsOverrides map[string]string return c } + +func requireIDPsListedByIDPDiscoveryEndpoint( + t *testing.T, + env *testlib.TestEnv, + ctx context.Context, + kubeClient kubernetes.Interface, + ns, supervisorScheme, supervisorAddress, supervisorCABundle, issuerName string) *supervisorconfigv1alpha1.FederationDomain { + // github + gitHubIDPSecretName := "github-idp-secret" //nolint:gosec // this is not a credential + _, err := kubeClient.CoreV1().Secrets(ns).Create(ctx, &corev1.Secret{ + Type: "secrets.pinniped.dev/github-client", + TypeMeta: metav1.TypeMeta{}, + ObjectMeta: metav1.ObjectMeta{ + Name: gitHubIDPSecretName, + Namespace: ns, + }, + StringData: map[string]string{ + "clientID": "foo", + "clientSecret": "bar", + }, + }, metav1.CreateOptions{}) + require.NoError(t, err) + t.Cleanup(func() { + _ = kubeClient.CoreV1().Secrets(ns).Delete(ctx, gitHubIDPSecretName, metav1.DeleteOptions{}) + }) + + ghIDP := testlib.CreateGitHubIdentityProvider(t, idpv1alpha1.GitHubIdentityProviderSpec{ + Client: idpv1alpha1.GitHubClientSpec{ + SecretName: gitHubIDPSecretName, + }, + AllowAuthentication: idpv1alpha1.GitHubAllowAuthenticationSpec{ + Organizations: idpv1alpha1.GitHubOrganizationsSpec{ + Policy: ptr.To(idpv1alpha1.GitHubAllowedAuthOrganizationsPolicyAllGitHubUsers), + }, + }, + }, idpv1alpha1.GitHubPhaseReady) + + ldapBindSecret := testlib.CreateTestSecret(t, env.SupervisorNamespace, "ldap-service-account", corev1.SecretTypeBasicAuth, + map[string]string{ + corev1.BasicAuthUsernameKey: env.SupervisorUpstreamLDAP.BindUsername, + corev1.BasicAuthPasswordKey: env.SupervisorUpstreamLDAP.BindPassword, + }, + ) + ldapIDP := testlib.CreateTestLDAPIdentityProvider(t, idpv1alpha1.LDAPIdentityProviderSpec{ + Host: env.SupervisorUpstreamLDAP.Host, + TLS: &idpv1alpha1.TLSSpec{ + CertificateAuthorityData: base64.StdEncoding.EncodeToString([]byte(env.SupervisorUpstreamLDAP.CABundle)), + }, + Bind: idpv1alpha1.LDAPIdentityProviderBind{ + SecretName: ldapBindSecret.Name, + }, + UserSearch: idpv1alpha1.LDAPIdentityProviderUserSearch{ + Base: env.SupervisorUpstreamLDAP.UserSearchBase, + Filter: "", + Attributes: idpv1alpha1.LDAPIdentityProviderUserSearchAttributes{ + Username: env.SupervisorUpstreamLDAP.TestUserMailAttributeName, + UID: env.SupervisorUpstreamLDAP.TestUserUniqueIDAttributeName, + }, + }, + GroupSearch: idpv1alpha1.LDAPIdentityProviderGroupSearch{ + Base: env.SupervisorUpstreamLDAP.GroupSearchBase, + Filter: "", // use the default value of "member={}" + Attributes: idpv1alpha1.LDAPIdentityProviderGroupSearchAttributes{ + GroupName: "", // use the default value of "dn" + }, + }, + }, idpv1alpha1.LDAPPhaseReady) + + oidcIDP := testlib.CreateTestOIDCIdentityProvider(t, idpv1alpha1.OIDCIdentityProviderSpec{ + Issuer: env.SupervisorUpstreamOIDC.Issuer, + TLS: &idpv1alpha1.TLSSpec{ + CertificateAuthorityData: base64.StdEncoding.EncodeToString([]byte(env.SupervisorUpstreamOIDC.CABundle)), + }, + AuthorizationConfig: idpv1alpha1.OIDCAuthorizationConfig{ + AdditionalScopes: env.SupervisorUpstreamOIDC.AdditionalScopes, + }, + Claims: idpv1alpha1.OIDCClaims{ + Username: env.SupervisorUpstreamOIDC.UsernameClaim, + Groups: env.SupervisorUpstreamOIDC.GroupsClaim, + }, + Client: idpv1alpha1.OIDCClient{ + SecretName: testlib.CreateOIDCClientCredentialsSecret(t, env.SupervisorUpstreamOIDC.ClientID, env.SupervisorUpstreamOIDC.ClientSecret).Name, + }, + }, idpv1alpha1.PhaseReady) + + var adIDP *idpv1alpha1.ActiveDirectoryIdentityProvider + if activeDirectoryAvailable(t, env) { + activeDirectoryBindSecret := testlib.CreateTestSecret(t, env.SupervisorNamespace, "ldap-service-account", corev1.SecretTypeBasicAuth, + map[string]string{ + corev1.BasicAuthUsernameKey: env.SupervisorUpstreamActiveDirectory.BindUsername, + corev1.BasicAuthPasswordKey: env.SupervisorUpstreamActiveDirectory.BindPassword, + }, + ) + adIDP = testlib.CreateTestActiveDirectoryIdentityProvider(t, idpv1alpha1.ActiveDirectoryIdentityProviderSpec{ + Host: env.SupervisorUpstreamActiveDirectory.Host, + TLS: &idpv1alpha1.TLSSpec{ + CertificateAuthorityData: base64.StdEncoding.EncodeToString([]byte(env.SupervisorUpstreamActiveDirectory.CABundle)), + }, + Bind: idpv1alpha1.ActiveDirectoryIdentityProviderBind{ + SecretName: activeDirectoryBindSecret.Name, + }, + }, idpv1alpha1.ActiveDirectoryPhaseReady) + } + + idpsForFD := []supervisorconfigv1alpha1.FederationDomainIdentityProvider{{ + DisplayName: ghIDP.Name, + ObjectRef: corev1.TypedLocalObjectReference{ + APIGroup: ptr.To("idp.supervisor." + env.APIGroupSuffix), + Kind: "GitHubIdentityProvider", + Name: ghIDP.Name, + }, + }, { + DisplayName: ldapIDP.Name, + ObjectRef: corev1.TypedLocalObjectReference{ + APIGroup: ptr.To("idp.supervisor." + env.APIGroupSuffix), + Kind: "LDAPIdentityProvider", + Name: ldapIDP.Name, + }, + }, { + DisplayName: oidcIDP.Name, + ObjectRef: corev1.TypedLocalObjectReference{ + APIGroup: ptr.To("idp.supervisor." + env.APIGroupSuffix), + Kind: "OIDCIdentityProvider", + Name: oidcIDP.Name, + }, + }} + if activeDirectoryAvailable(t, env) { + idpsForFD = append(idpsForFD, supervisorconfigv1alpha1.FederationDomainIdentityProvider{ + DisplayName: adIDP.Name, + ObjectRef: corev1.TypedLocalObjectReference{ + APIGroup: ptr.To("idp.supervisor." + env.APIGroupSuffix), + Kind: "ActiveDirectoryIdentityProvider", + Name: adIDP.Name, + }, + }) + } + federationDomainConfig := testlib.CreateTestFederationDomain(ctx, t, supervisorconfigv1alpha1.FederationDomainSpec{ + Issuer: issuerName, + IdentityProviders: idpsForFD, + }, supervisorconfigv1alpha1.FederationDomainPhaseReady) + + requireStandardDiscoveryEndpointsAreWorking(t, supervisorScheme, supervisorAddress, supervisorCABundle, issuerName, nil) + issuer8URL, err := url.Parse(issuerName) + require.NoError(t, err) + wellKnownURL := wellKnownURLForIssuer(supervisorScheme, supervisorAddress, issuer8URL.Path) + _, wellKnownResponseBody := requireSuccessEndpointResponse(t, wellKnownURL, issuerName, supervisorCABundle, nil) //nolint:bodyclose + + type WellKnownResponse struct { + Issuer string `json:"issuer"` + AuthorizationEndpoint string `json:"authorization_endpoint"` + TokenEndpoint string `json:"token_endpoint"` + JWKSUri string `json:"jwks_uri"` + DiscoverySupervisor struct { + IdentityProvidersEndpoint string `json:"pinniped_identity_providers_endpoint"` + } `json:"discovery.supervisor.pinniped.dev/v1alpha1"` + } + var wellKnownResponse WellKnownResponse + err = json.Unmarshal([]byte(wellKnownResponseBody), &wellKnownResponse) + require.NoError(t, err) + discoveryIDPEndpoint := wellKnownResponse.DiscoverySupervisor.IdentityProvidersEndpoint + _, discoveryIDPResponseBody := requireSuccessEndpointResponse(t, discoveryIDPEndpoint, issuerName, supervisorCABundle, nil) //nolint:bodyclose + type IdentityProviderListResponse struct { + IdentityProviders []struct { + Name string `json:"name"` + Type string `json:"type"` + } `json:"pinniped_identity_providers"` + } + var identityProviderListResponse IdentityProviderListResponse + err = json.Unmarshal([]byte(discoveryIDPResponseBody), &identityProviderListResponse) + require.NoError(t, err) + + allIDPs := []string{ghIDP.Name, ldapIDP.Name, oidcIDP.Name} + if activeDirectoryAvailable(t, env) { + allIDPs = append(allIDPs, adIDP.Name) + } + require.Equal(t, len(identityProviderListResponse.IdentityProviders), len(allIDPs), "all IDPs should be listed by idp discovery endpoint") + for _, provider := range identityProviderListResponse.IdentityProviders { + require.Contains(t, allIDPs, provider.Name, fmt.Sprintf("provider name should be listed in IDP discovery: %s", provider.Name)) + } + + return federationDomainConfig +} + +func activeDirectoryAvailable(t *testing.T, env *testlib.TestEnv) bool { + t.Helper() + hasLDAPPorts := env.HasCapability(testlib.CanReachInternetLDAPPorts) + hasADHost := testlib.IntegrationEnv(t).SupervisorUpstreamActiveDirectory.Host != "" + return hasLDAPPorts && hasADHost +} diff --git a/test/integration/supervisor_federationdomain_status_test.go b/test/integration/supervisor_federationdomain_status_test.go index 2cf158bc7..03ae38e56 100644 --- a/test/integration/supervisor_federationdomain_status_test.go +++ b/test/integration/supervisor_federationdomain_status_test.go @@ -362,7 +362,7 @@ func TestSupervisorFederationDomainStatus_Disruptive(t *testing.T) { { Type: "IdentityProvidersObjectRefKindValid", Status: "False", Reason: "KindUnrecognized", Message: `some kinds specified by .spec.identityProviders[].objectRef.kind are not recognized ` + - `(should be one of "ActiveDirectoryIdentityProvider", "LDAPIdentityProvider", "OIDCIdentityProvider"): "this is the wrong kind"`, + `(should be one of "ActiveDirectoryIdentityProvider", "GitHubIdentityProvider", "LDAPIdentityProvider", "OIDCIdentityProvider"): "this is the wrong kind"`, }, { Type: "Ready", Status: "False", Reason: "NotReady", @@ -493,7 +493,7 @@ func TestSupervisorFederationDomainStatus_Disruptive(t *testing.T) { { Type: "IdentityProvidersObjectRefKindValid", Status: "False", Reason: "KindUnrecognized", Message: `some kinds specified by .spec.identityProviders[].objectRef.kind are not recognized ` + - `(should be one of "ActiveDirectoryIdentityProvider", "LDAPIdentityProvider", "OIDCIdentityProvider"): "this is the wrong kind"`, + `(should be one of "ActiveDirectoryIdentityProvider", "GitHubIdentityProvider", "LDAPIdentityProvider", "OIDCIdentityProvider"): "this is the wrong kind"`, }, { Type: "Ready", Status: "False", Reason: "NotReady", diff --git a/test/integration/supervisor_github_idp_test.go b/test/integration/supervisor_github_idp_test.go new file mode 100644 index 000000000..b250cdf3f --- /dev/null +++ b/test/integration/supervisor_github_idp_test.go @@ -0,0 +1,746 @@ +// Copyright 2024 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package integration + +import ( + "context" + "encoding/base64" + "fmt" + "os" + "path/filepath" + "testing" + "time" + + "github.com/stretchr/testify/require" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/utils/ptr" + + idpv1alpha1 "go.pinniped.dev/generated/latest/apis/supervisor/idp/v1alpha1" + "go.pinniped.dev/internal/here" + "go.pinniped.dev/internal/testutil" + + "go.pinniped.dev/test/testlib" +) + +const generateNamePrefix = "integration-test-github-idp-" + +func TestGitHubIDPStaticValidationOnCreate_Parallel(t *testing.T) { + adminClient := testlib.NewKubernetesClientset(t) + + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Minute) + t.Cleanup(cancel) + + namespaceClient := adminClient.CoreV1().Namespaces() + skipCELTests := !testutil.KubeServerMinorVersionAtLeastInclusive(t, adminClient.Discovery(), 26) + + ns, err := namespaceClient.Create(ctx, &corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + GenerateName: generateNamePrefix, + }, + }, metav1.CreateOptions{}) + require.NoError(t, err) + + t.Cleanup(func() { + require.NoError(t, namespaceClient.Delete(ctx, ns.Name, metav1.DeleteOptions{})) + }) + + gitHubIDPClient := testlib.NewSupervisorClientset(t).IDPV1alpha1().GitHubIdentityProviders(ns.Name) + + tests := []struct { + name string + usesCELValidation bool + inputSpec idpv1alpha1.GitHubIdentityProviderSpec + wantSpec idpv1alpha1.GitHubIdentityProviderSpec + wantErr string + }{ + { + name: "all fields set", + inputSpec: idpv1alpha1.GitHubIdentityProviderSpec{ + GitHubAPI: idpv1alpha1.GitHubAPIConfig{ + Host: ptr.To("some-host.example.com"), + TLS: &idpv1alpha1.TLSSpec{ + CertificateAuthorityData: func() string { + return base64.StdEncoding.EncodeToString([]byte("-----BEGIN CERTIFICATE-----\ndata goes here")) + }(), + }, + }, + AllowAuthentication: idpv1alpha1.GitHubAllowAuthenticationSpec{ + Organizations: idpv1alpha1.GitHubOrganizationsSpec{ + Allowed: []string{ + "org1", + "that-other-org", + }, + Policy: ptr.To(idpv1alpha1.GitHubAllowedAuthOrganizationsPolicyOnlyUsersFromAllowedOrganizations), + }, + }, + Claims: idpv1alpha1.GitHubClaims{ + Username: ptr.To(idpv1alpha1.GitHubUsernameLoginAndID), + Groups: ptr.To(idpv1alpha1.GitHubUseTeamSlugForGroupName), + }, + Client: idpv1alpha1.GitHubClientSpec{ + SecretName: "any-name-goes-here", + }, + }, + wantSpec: idpv1alpha1.GitHubIdentityProviderSpec{ + GitHubAPI: idpv1alpha1.GitHubAPIConfig{ + Host: ptr.To("some-host.example.com"), + TLS: &idpv1alpha1.TLSSpec{ + CertificateAuthorityData: "LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCmRhdGEgZ29lcyBoZXJl", + }, + }, + AllowAuthentication: idpv1alpha1.GitHubAllowAuthenticationSpec{ + Organizations: idpv1alpha1.GitHubOrganizationsSpec{ + Allowed: []string{ + "org1", + "that-other-org", + }, + Policy: ptr.To(idpv1alpha1.GitHubAllowedAuthOrganizationsPolicyOnlyUsersFromAllowedOrganizations), + }, + }, + Claims: idpv1alpha1.GitHubClaims{ + Username: ptr.To(idpv1alpha1.GitHubUsernameLoginAndID), + Groups: ptr.To(idpv1alpha1.GitHubUseTeamSlugForGroupName), + }, + Client: idpv1alpha1.GitHubClientSpec{ + SecretName: "any-name-goes-here", + }, + }, + }, + { + name: "minimum fields set - inherit defaults", + inputSpec: idpv1alpha1.GitHubIdentityProviderSpec{ + AllowAuthentication: idpv1alpha1.GitHubAllowAuthenticationSpec{ + Organizations: idpv1alpha1.GitHubOrganizationsSpec{ + Policy: ptr.To(idpv1alpha1.GitHubAllowedAuthOrganizationsPolicyAllGitHubUsers), + }, + }, + Client: idpv1alpha1.GitHubClientSpec{ + SecretName: "name-of-a-secret", + }, + }, + wantSpec: idpv1alpha1.GitHubIdentityProviderSpec{ + GitHubAPI: idpv1alpha1.GitHubAPIConfig{ + Host: ptr.To("github.com"), + }, + AllowAuthentication: idpv1alpha1.GitHubAllowAuthenticationSpec{ + Organizations: idpv1alpha1.GitHubOrganizationsSpec{ + Policy: ptr.To(idpv1alpha1.GitHubAllowedAuthOrganizationsPolicyAllGitHubUsers), + }, + }, + Claims: idpv1alpha1.GitHubClaims{ + Username: ptr.To(idpv1alpha1.GitHubUsernameLoginAndID), + Groups: ptr.To(idpv1alpha1.GitHubUseTeamSlugForGroupName), + }, + Client: idpv1alpha1.GitHubClientSpec{ + SecretName: "name-of-a-secret", + }, + }, + }, + { + name: fmt.Sprintf( + "cannot set AllowedOrganizationsPolicy=%s and set AllowedOrganizations", + string(idpv1alpha1.GitHubAllowedAuthOrganizationsPolicyAllGitHubUsers)), + usesCELValidation: true, + inputSpec: idpv1alpha1.GitHubIdentityProviderSpec{ + AllowAuthentication: idpv1alpha1.GitHubAllowAuthenticationSpec{ + Organizations: idpv1alpha1.GitHubOrganizationsSpec{ + Allowed: []string{ + "some-org", + }, + Policy: ptr.To(idpv1alpha1.GitHubAllowedAuthOrganizationsPolicyAllGitHubUsers), + }, + }, + Client: idpv1alpha1.GitHubClientSpec{ + SecretName: "name-of-a-secret", + }, + }, + wantErr: "spec.allowAuthentication.organizations.policy must be 'OnlyUsersFromAllowedOrganizations' when spec.allowAuthentication.organizations.allowed has organizations listed", + }, + { + name: fmt.Sprintf("spec.allowAuthentication.organizations.policy must be '%s' when spec.allowAuthentication.organizations.allowed is empty (nil)", string(idpv1alpha1.GitHubAllowedAuthOrganizationsPolicyAllGitHubUsers)), + usesCELValidation: true, + inputSpec: idpv1alpha1.GitHubIdentityProviderSpec{ + AllowAuthentication: idpv1alpha1.GitHubAllowAuthenticationSpec{ + Organizations: idpv1alpha1.GitHubOrganizationsSpec{ + Policy: ptr.To(idpv1alpha1.GitHubAllowedAuthOrganizationsPolicyOnlyUsersFromAllowedOrganizations), + }, + }, + Client: idpv1alpha1.GitHubClientSpec{ + SecretName: "name-of-a-secret", + }, + }, + wantErr: "spec.allowAuthentication.organizations.policy must be 'AllGitHubUsers' when spec.allowAuthentication.organizations.allowed is empty", + }, + { + name: fmt.Sprintf("spec.allowAuthentication.organizations.policy must be '%s' when spec.allowAuthentication.organizations.allowed is empty", string(idpv1alpha1.GitHubAllowedAuthOrganizationsPolicyAllGitHubUsers)), + usesCELValidation: true, + inputSpec: idpv1alpha1.GitHubIdentityProviderSpec{ + AllowAuthentication: idpv1alpha1.GitHubAllowAuthenticationSpec{ + Organizations: idpv1alpha1.GitHubOrganizationsSpec{ + Allowed: []string{}, + Policy: ptr.To(idpv1alpha1.GitHubAllowedAuthOrganizationsPolicyOnlyUsersFromAllowedOrganizations), + }, + }, + Client: idpv1alpha1.GitHubClientSpec{ + SecretName: "name-of-a-secret", + }, + }, + wantErr: "spec.allowAuthentication.organizations.policy must be 'AllGitHubUsers' when spec.allowAuthentication.organizations.allowed is empty", + }, + { + name: "spec.client.secretName in body should be at least 1 chars long", + inputSpec: idpv1alpha1.GitHubIdentityProviderSpec{}, + wantErr: "spec.client.secretName in body should be at least 1 chars long", + }, + { + name: "spec.githubAPI.host in body should be at least 1 chars long", + inputSpec: idpv1alpha1.GitHubIdentityProviderSpec{ + GitHubAPI: idpv1alpha1.GitHubAPIConfig{ + Host: ptr.To(""), + }, + AllowAuthentication: idpv1alpha1.GitHubAllowAuthenticationSpec{ + Organizations: idpv1alpha1.GitHubOrganizationsSpec{ + Policy: ptr.To(idpv1alpha1.GitHubAllowedAuthOrganizationsPolicyAllGitHubUsers), + }, + }, + Client: idpv1alpha1.GitHubClientSpec{ + SecretName: "name-of-a-secret", + }, + }, + wantErr: "spec.githubAPI.host in body should be at least 1 chars long", + }, + { + name: "duplicates not permitted in spec.allowAuthentication.organizations.allowed", + inputSpec: idpv1alpha1.GitHubIdentityProviderSpec{ + AllowAuthentication: idpv1alpha1.GitHubAllowAuthenticationSpec{ + Organizations: idpv1alpha1.GitHubOrganizationsSpec{ + Allowed: []string{ + "org1", + "org1", + }, + Policy: ptr.To(idpv1alpha1.GitHubAllowedAuthOrganizationsPolicyOnlyUsersFromAllowedOrganizations), + }, + }, + Client: idpv1alpha1.GitHubClientSpec{ + SecretName: "name-of-a-secret", + }, + }, + wantErr: `spec.allowAuthentication.organizations.allowed[1]: Duplicate value: "org1"`, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + if tt.usesCELValidation && skipCELTests { + t.Skip("CEL is not available for current K8s version") + } + + input := &idpv1alpha1.GitHubIdentityProvider{ + ObjectMeta: metav1.ObjectMeta{ + GenerateName: generateNamePrefix, + }, + Spec: tt.inputSpec, + } + + outputGitHubIDP, err := gitHubIDPClient.Create(ctx, input, metav1.CreateOptions{}) + if tt.wantErr == "" { + require.NoError(t, err) + require.Equal(t, tt.wantSpec, outputGitHubIDP.Spec) + } else { + require.ErrorContains(t, err, tt.wantErr) + } + }) + } +} + +func TestGitHubIDPSetsDefaultsWithKubectl_Parallel(t *testing.T) { + env := testlib.IntegrationEnv(t) + + adminClient := testlib.NewKubernetesClientset(t) + + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Minute) + t.Cleanup(cancel) + + namespaceClient := adminClient.CoreV1().Namespaces() + + ns, err := namespaceClient.Create(ctx, &corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + GenerateName: generateNamePrefix, + }, + }, metav1.CreateOptions{}) + require.NoError(t, err) + + t.Cleanup(func() { + require.NoError(t, namespaceClient.Delete(ctx, ns.Name, metav1.DeleteOptions{})) + }) + t.Logf("Created namespace %s", ns.Name) + + idpName := generateNamePrefix + testlib.RandHex(t, 16) + + githubIDPYaml := []byte(here.Doc(fmt.Sprintf(` + --- + apiVersion: idp.supervisor.%s/v1alpha1 + kind: GitHubIdentityProvider + metadata: + name: %s + namespace: %s + spec: + allowAuthentication: + organizations: + policy: AllGitHubUsers + client: + secretName: any-secret-name`, env.APIGroupSuffix, idpName, ns.Name))) + + githubIDPYamlFilepath := filepath.Join(t.TempDir(), "github-idp.yaml") + + require.NoError(t, os.WriteFile(githubIDPYamlFilepath, githubIDPYaml, 0600)) + + stdOut, stdErr := runTestKubectlCommand(t, "create", "-f", githubIDPYamlFilepath) + + require.Equal(t, fmt.Sprintf("githubidentityprovider.idp.supervisor.%s/%s created\n", env.APIGroupSuffix, idpName), stdOut) + require.Empty(t, stdErr) + + gitHubIDPClient := testlib.NewSupervisorClientset(t).IDPV1alpha1().GitHubIdentityProviders(ns.Name) + + idp, err := gitHubIDPClient.Get(ctx, idpName, metav1.GetOptions{}) + require.NoError(t, err) + require.Equal(t, idpv1alpha1.GitHubIdentityProviderSpec{ + GitHubAPI: idpv1alpha1.GitHubAPIConfig{ + Host: ptr.To("github.com"), + }, + Claims: idpv1alpha1.GitHubClaims{ + Username: ptr.To(idpv1alpha1.GitHubUsernameLoginAndID), + Groups: ptr.To(idpv1alpha1.GitHubUseTeamSlugForGroupName), + }, + AllowAuthentication: idpv1alpha1.GitHubAllowAuthenticationSpec{ + Organizations: idpv1alpha1.GitHubOrganizationsSpec{ + Policy: ptr.To(idpv1alpha1.GitHubAllowedAuthOrganizationsPolicyAllGitHubUsers), + }, + }, + Client: idpv1alpha1.GitHubClientSpec{ + SecretName: "any-secret-name", + }, + }, idp.Spec) +} + +func TestGitHubIDPPhaseAndConditions_Parallel(t *testing.T) { + // These operations must be performed in the Supervisor's namespace so that the controller can find GitHubIdentityProvider + supervisorNamespace := testlib.IntegrationEnv(t).SupervisorNamespace + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Minute) + t.Cleanup(cancel) + + kubernetesClient := testlib.NewKubernetesClientset(t) + secretsClient := kubernetesClient.CoreV1().Secrets(supervisorNamespace) + gitHubIDPClient := testlib.NewSupervisorClientset(t).IDPV1alpha1().GitHubIdentityProviders(supervisorNamespace) + + happySecretName := generateNamePrefix + testlib.RandHex(t, 16) + invalidSecretName := generateNamePrefix + testlib.RandHex(t, 16) + + tests := []struct { + name string + secrets []*corev1.Secret // Secrets will be created first, and the first secret found will be listed as the configured GitHub Client secret + idps []*idpv1alpha1.GitHubIdentityProvider + wantPhase idpv1alpha1.GitHubIdentityProviderPhase + wantConditions []*metav1.Condition + }{ + { + name: "Happy Path", + secrets: []*corev1.Secret{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: happySecretName, + }, + Type: "secrets.pinniped.dev/github-client", + Data: map[string][]byte{ + "clientID": []byte("foo"), + "clientSecret": []byte("bar"), + }, + }, + }, + idps: []*idpv1alpha1.GitHubIdentityProvider{ + { + Spec: idpv1alpha1.GitHubIdentityProviderSpec{ + GitHubAPI: idpv1alpha1.GitHubAPIConfig{ + Host: ptr.To("github.com"), + }, + AllowAuthentication: idpv1alpha1.GitHubAllowAuthenticationSpec{ + Organizations: idpv1alpha1.GitHubOrganizationsSpec{ + Policy: ptr.To(idpv1alpha1.GitHubAllowedAuthOrganizationsPolicyAllGitHubUsers), + }, + }, + }, + }, + }, + wantPhase: idpv1alpha1.GitHubPhaseReady, + wantConditions: []*metav1.Condition{ + { + Type: "ClaimsValid", + Status: metav1.ConditionTrue, + Reason: "Success", + Message: "spec.claims are valid", + }, + { + Type: "ClientCredentialsSecretValid", + Status: metav1.ConditionTrue, + Reason: "Success", + Message: fmt.Sprintf("clientID and clientSecret have been read from spec.client.SecretName (%q)", happySecretName), + }, + { + Type: "GitHubConnectionValid", + Status: metav1.ConditionTrue, + Reason: "Success", + Message: `spec.githubAPI.host ("github.com:443") is reachable and TLS verification succeeds`, + }, + { + Type: "HostValid", + Status: metav1.ConditionTrue, + Reason: "Success", + Message: `spec.githubAPI.host ("github.com") is valid`, + }, + { + Type: "OrganizationsPolicyValid", + Status: metav1.ConditionTrue, + Reason: "Success", + Message: `spec.allowAuthentication.organizations.policy ("AllGitHubUsers") is valid`, + }, + { + Type: "TLSConfigurationValid", + Status: metav1.ConditionTrue, + Reason: "Success", + Message: "spec.githubAPI.tls.certificateAuthorityData is valid", + }, + }, + }, + { + name: "Invalid Client Secret", + secrets: []*corev1.Secret{ + { + Type: "secrets.pinniped.dev/github-client", + ObjectMeta: metav1.ObjectMeta{ + Name: invalidSecretName, + }, + }, + }, + idps: []*idpv1alpha1.GitHubIdentityProvider{ + { + Spec: idpv1alpha1.GitHubIdentityProviderSpec{ + GitHubAPI: idpv1alpha1.GitHubAPIConfig{ + Host: ptr.To("github.com"), + }, + AllowAuthentication: idpv1alpha1.GitHubAllowAuthenticationSpec{ + Organizations: idpv1alpha1.GitHubOrganizationsSpec{ + Policy: ptr.To(idpv1alpha1.GitHubAllowedAuthOrganizationsPolicyAllGitHubUsers), + }, + }, + Client: idpv1alpha1.GitHubClientSpec{ + SecretName: invalidSecretName, + }, + }, + }, + }, + wantPhase: idpv1alpha1.GitHubPhaseError, + wantConditions: []*metav1.Condition{ + { + Type: "ClaimsValid", + Status: metav1.ConditionTrue, + Reason: "Success", + Message: "spec.claims are valid", + }, + { + Type: "ClientCredentialsSecretValid", + Status: metav1.ConditionFalse, + Reason: "SecretNotFound", + Message: fmt.Sprintf(`missing key "clientID": secret from spec.client.SecretName (%q) must be found in namespace %q with type "secrets.pinniped.dev/github-client" and keys "clientID" and "clientSecret"`, + invalidSecretName, + supervisorNamespace), + }, + { + Type: "GitHubConnectionValid", + Status: metav1.ConditionTrue, + Reason: "Success", + Message: `spec.githubAPI.host ("github.com:443") is reachable and TLS verification succeeds`, + }, + { + Type: "HostValid", + Status: metav1.ConditionTrue, + Reason: "Success", + Message: `spec.githubAPI.host ("github.com") is valid`, + }, + { + Type: "OrganizationsPolicyValid", + Status: metav1.ConditionTrue, + Reason: "Success", + Message: `spec.allowAuthentication.organizations.policy ("AllGitHubUsers") is valid`, + }, + { + Type: "TLSConfigurationValid", + Status: metav1.ConditionTrue, + Reason: "Success", + Message: `spec.githubAPI.tls.certificateAuthorityData is valid`, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + var secretName string + for _, secret := range tt.secrets { + secret.GenerateName = generateNamePrefix + + created, err := secretsClient.Create(ctx, secret, metav1.CreateOptions{}) + require.NoError(t, err) + t.Cleanup(func() { + err := secretsClient.Delete(ctx, created.Name, metav1.DeleteOptions{}) + require.NoError(t, err) + }) + if secretName == "" { + secretName = created.Name + } + } + + for _, idp := range tt.idps { + idp.Name = "" + idp.GenerateName = generateNamePrefix + idp.Spec.Client.SecretName = secretName + + created, err := gitHubIDPClient.Create(ctx, idp, metav1.CreateOptions{}) + require.NoError(t, err) + + t.Cleanup(func() { + err := gitHubIDPClient.Delete(ctx, created.Name, metav1.DeleteOptions{}) + require.NoError(t, err) + }) + testlib.WaitForGitHubIDPPhase(ctx, t, gitHubIDPClient, created.Name, tt.wantPhase) + testlib.WaitForGitHubIdentityProviderStatusConditions(ctx, t, gitHubIDPClient, created.Name, tt.wantConditions) + } + }) + } +} + +func TestGitHubIDPInWrongNamespace_Parallel(t *testing.T) { + // The GitHubIdentityProvider must be in the same namespace as the controller + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Minute) + t.Cleanup(cancel) + + kubernetesClient := testlib.NewKubernetesClientset(t) + + namespaceClient := kubernetesClient.CoreV1().Namespaces() + otherNamespace, err := namespaceClient.Create(ctx, &corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + GenerateName: generateNamePrefix, + }, + }, metav1.CreateOptions{}) + require.NoError(t, err) + + t.Cleanup(func() { + require.NoError(t, namespaceClient.Delete(ctx, otherNamespace.Name, metav1.DeleteOptions{})) + }) + + gitHubIDPClient := testlib.NewSupervisorClientset(t).IDPV1alpha1().GitHubIdentityProviders(otherNamespace.Name) + + idp := &idpv1alpha1.GitHubIdentityProvider{ + ObjectMeta: metav1.ObjectMeta{ + GenerateName: generateNamePrefix, + Namespace: otherNamespace.Name, + }, + Spec: idpv1alpha1.GitHubIdentityProviderSpec{ + GitHubAPI: idpv1alpha1.GitHubAPIConfig{ + Host: ptr.To("github.com"), + }, + AllowAuthentication: idpv1alpha1.GitHubAllowAuthenticationSpec{ + Organizations: idpv1alpha1.GitHubOrganizationsSpec{ + Policy: ptr.To(idpv1alpha1.GitHubAllowedAuthOrganizationsPolicyAllGitHubUsers), + }, + }, + Client: idpv1alpha1.GitHubClientSpec{ + SecretName: "does-not-matter", + }, + }, + } + + createdIDP, err := gitHubIDPClient.Create(ctx, idp, metav1.CreateOptions{}) + require.NoError(t, err) + + t.Cleanup(func() { + err := gitHubIDPClient.Delete(ctx, createdIDP.Name, metav1.DeleteOptions{}) + require.NoError(t, err) + }) + + // We require that there's never an error + // ... and that the status phase is never anything but Pending + // ... and that there are no status conditions + require.Never(t, func() bool { + idp, err := gitHubIDPClient.Get(ctx, createdIDP.Name, metav1.GetOptions{}) + return err != nil && idp.Status.Phase != idpv1alpha1.GitHubPhasePending && len(idp.Status.Conditions) > 0 + }, 2*time.Minute, 10*time.Second) +} + +func TestGitHubIDPSecretInOtherNamespace_Parallel(t *testing.T) { + // The GitHubIdentityProvider must be in the same namespace as the controller + supervisorNamespace := testlib.IntegrationEnv(t).SupervisorNamespace + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Minute) + t.Cleanup(cancel) + + kubernetesClient := testlib.NewKubernetesClientset(t) + gitHubIDPClient := testlib.NewSupervisorClientset(t).IDPV1alpha1().GitHubIdentityProviders(supervisorNamespace) + + namespaceClient := kubernetesClient.CoreV1().Namespaces() + otherNamespace, err := namespaceClient.Create(ctx, &corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + GenerateName: generateNamePrefix, + }, + }, metav1.CreateOptions{}) + require.NoError(t, err) + + t.Cleanup(func() { + require.NoError(t, namespaceClient.Delete(ctx, otherNamespace.Name, metav1.DeleteOptions{})) + }) + + secretsClient := kubernetesClient.CoreV1().Secrets(otherNamespace.Name) + + secret := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + GenerateName: generateNamePrefix, + Namespace: otherNamespace.Name, + }, + Type: "secrets.pinniped.dev/github-client", + Data: map[string][]byte{ + "clientID": []byte("foo"), + "clientSecret": []byte("bar"), + }, + } + + // This secret will be cleaned up when its namespace is deleted + createdSecret, err := secretsClient.Create(ctx, secret, metav1.CreateOptions{}) + require.NoError(t, err) + + idp := &idpv1alpha1.GitHubIdentityProvider{ + ObjectMeta: metav1.ObjectMeta{ + GenerateName: generateNamePrefix, + Namespace: supervisorNamespace, + }, + Spec: idpv1alpha1.GitHubIdentityProviderSpec{ + GitHubAPI: idpv1alpha1.GitHubAPIConfig{ + Host: ptr.To("github.com"), + }, + AllowAuthentication: idpv1alpha1.GitHubAllowAuthenticationSpec{ + Organizations: idpv1alpha1.GitHubOrganizationsSpec{ + Policy: ptr.To(idpv1alpha1.GitHubAllowedAuthOrganizationsPolicyAllGitHubUsers), + }, + }, + Client: idpv1alpha1.GitHubClientSpec{ + SecretName: createdSecret.Name, + }, + }, + } + + created, err := gitHubIDPClient.Create(ctx, idp, metav1.CreateOptions{}) + require.NoError(t, err) + + t.Cleanup(func() { + err := gitHubIDPClient.Delete(ctx, created.Name, metav1.DeleteOptions{}) + require.NoError(t, err) + }) + testlib.WaitForGitHubIDPPhase(ctx, t, gitHubIDPClient, created.Name, idpv1alpha1.GitHubPhaseError) + + testlib.WaitForGitHubIdentityProviderStatusConditions(ctx, t, gitHubIDPClient, created.Name, []*metav1.Condition{ + { + Type: "ClaimsValid", + Status: metav1.ConditionTrue, + Reason: "Success", + Message: "spec.claims are valid", + }, + { + Type: "ClientCredentialsSecretValid", + Status: metav1.ConditionFalse, + Reason: "SecretNotFound", + Message: fmt.Sprintf(`secret %q not found: secret from spec.client.SecretName (%q) must be found in namespace %q with type "secrets.pinniped.dev/github-client" and keys "clientID" and "clientSecret"`, + idp.Spec.Client.SecretName, + idp.Spec.Client.SecretName, + supervisorNamespace), + }, + { + Type: "GitHubConnectionValid", + Status: metav1.ConditionTrue, + Reason: "Success", + Message: `spec.githubAPI.host ("github.com:443") is reachable and TLS verification succeeds`, + }, + { + Type: "HostValid", + Status: metav1.ConditionTrue, + Reason: "Success", + Message: `spec.githubAPI.host ("github.com") is valid`, + }, + { + Type: "OrganizationsPolicyValid", + Status: metav1.ConditionTrue, + Reason: "Success", + Message: `spec.allowAuthentication.organizations.policy ("AllGitHubUsers") is valid`, + }, + { + Type: "TLSConfigurationValid", + Status: metav1.ConditionTrue, + Reason: "Success", + Message: "spec.githubAPI.tls.certificateAuthorityData is valid", + }, + }) +} + +func TestGitHubIDPTooManyOrganizationsStaticValidationOnCreate_Parallel(t *testing.T) { + adminClient := testlib.NewKubernetesClientset(t) + + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Minute) + t.Cleanup(cancel) + + namespaceClient := adminClient.CoreV1().Namespaces() + + ns, err := namespaceClient.Create(ctx, &corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + GenerateName: generateNamePrefix, + }, + }, metav1.CreateOptions{}) + require.NoError(t, err) + + t.Cleanup(func() { + require.NoError(t, namespaceClient.Delete(ctx, ns.Name, metav1.DeleteOptions{})) + }) + + gitHubIDPClient := testlib.NewSupervisorClientset(t).IDPV1alpha1().GitHubIdentityProviders(ns.Name) + + input := &idpv1alpha1.GitHubIdentityProvider{ + ObjectMeta: metav1.ObjectMeta{ + GenerateName: generateNamePrefix, + }, + Spec: idpv1alpha1.GitHubIdentityProviderSpec{ + AllowAuthentication: idpv1alpha1.GitHubAllowAuthenticationSpec{ + Organizations: idpv1alpha1.GitHubOrganizationsSpec{ + Allowed: func() []string { + orgs := make([]string, 100) + for i := range 100 { + orgs[i] = fmt.Sprintf("org-%d", i) + } + return orgs + }(), + Policy: ptr.To(idpv1alpha1.GitHubAllowedAuthOrganizationsPolicyOnlyUsersFromAllowedOrganizations), + }, + }, + Client: idpv1alpha1.GitHubClientSpec{ + SecretName: "name-of-a-secret", + }, + }, + } + + _, err = gitHubIDPClient.Create(ctx, input, metav1.CreateOptions{}) + + wantErr := "spec.allowAuthentication.organizations.allowed: Invalid value: 100: spec.allowAuthentication.organizations.allowed in body should have at most 64 items" + if testutil.KubeServerMinorVersionAtLeastInclusive(t, adminClient.Discovery(), 24) { + wantErr = "spec.allowAuthentication.organizations.allowed: Too many: 100: must have at most 64 items" + } + + require.ErrorContains(t, err, wantErr) +} diff --git a/test/integration/supervisor_login_test.go b/test/integration/supervisor_login_test.go index 21c53fa5b..b41fa4099 100644 --- a/test/integration/supervisor_login_test.go +++ b/test/integration/supervisor_login_test.go @@ -44,6 +44,113 @@ import ( "go.pinniped.dev/test/testlib/browsertest" ) +// These tests attempt to exercise the entire login and refresh flow of the Supervisor for various cases. +// They do not use the Pinniped CLI as the client, which allows them to exercise the Supervisor as an +// OIDC provider in ways that the CLI might not use. Similar tests exist using the CLI in e2e_test.go. +// +// Each of these tests perform the following flow: +// 1. Configure an IDP CR. +// 2. Create a FederationDomain with TLS configured and wait for its JWKS endpoint to be available. +// 3. Call the authorization endpoint and log in as a specific user. +// Note that these tests do not use form_post response type (which is tested by e2e_test.go). +// 4. Listen on a local callback server for the authorization redirect, and assert that it was success or failure. +// 5. Call the token endpoint to exchange the authcode. +// 6. Call the token endpoint to perform the RFC8693 token exchange for the cluster-scoped ID token. +// 7. Potentially edit the refresh session data or IDP settings before the refresh. +// 8. Call the token endpoint to perform a refresh, and expect it to succeed. +// 9. Call the token endpoint again to perform another RFC8693 token exchange for the cluster-scoped ID token, +// this time using the recently refreshed tokens when submitting the request. +// 10. Potentially edit the refresh session data or IDP settings again, this time in such a way that the next +// refresh should fail. If done, then perform one more refresh and expect failure. +type supervisorLoginTestcase struct { + name string + + // This required function might choose to skip the test case, for example if the LDAP server is not + // available for an LDAP test. + maybeSkip func(t *testing.T) + + // This required function should configure an IDP CR. It should also wait for it to be ready and schedule + // its cleanup. Return the name of the IDP CR. + createIDP func(t *testing.T) string + + // Optionally specify the identityProviders part of the FederationDomain's spec by returning it from this function. + // Also return the displayName of the IDP that should be used during authentication (or empty string for no IDP name in the auth request). + // This function takes the name of the IDP CR which was returned by createIDP() as as argument. + federationDomainIDPs func(t *testing.T, idpName string) (idps []configv1alpha1.FederationDomainIdentityProvider, useIDPDisplayName string) + + // Optionally create an OIDCClient CR for the test to use. Return the client ID and client secret for the + // test to use. When not set, the test will default to using the "pinniped-cli" static client with no secret. + // When a client secret is returned, it will be used for authcode exchange, refresh requests, and RFC8693 + // token exchanges for cluster-scoped tokens (client secrets are not needed in authorization requests). + createOIDCClient func(t *testing.T, callbackURL string) (string, string) + + // Optionally return the username and password for the test to use when logging in. This username/password + // will be passed to requestAuthorization(), or empty strings will be passed to indicate that the defaults + // should be used. If there is any cleanup required, then this function should also schedule that cleanup. + testUser func(t *testing.T) (string, string) + + // This required function should call the authorization endpoint using the given URL and also perform whatever + // interactions are needed to log in as the user. + requestAuthorization func(t *testing.T, downstreamIssuer, downstreamAuthorizeURL, downstreamCallbackURL, username, password string, httpClient *http.Client) + + // This string will be used as the requested audience in the RFC8693 token exchange for + // the cluster-scoped ID token. When it is not specified, a default string will be used. + requestTokenExchangeAud string + + // The scopes to request from the authorization endpoint. Defaults will be used when not specified. + downstreamScopes []string + // The scopes to want granted from the authorization endpoint. Defaults to the downstreamScopes value when not, + // specified, i.e. by default it expects that all requested scopes were granted. + wantDownstreamScopes []string + + // When we want the localhost callback to have never happened, then the flow will stop there. The login was + // unable to finish so there is nothing to assert about what should have happened with the callback, and there + // won't be any error sent to the callback either. This would happen, for example, when the user fails to log + // in at the LDAP/AD login page, because then they would be redirected back to that page again, instead of + // getting a callback success/error redirect. + wantLocalhostCallbackToNeverHappen bool + + // The expected ID token subject claim value as a regexp, for the original ID token and the refreshed ID token. + wantDownstreamIDTokenSubjectToMatch string + // The expected ID token username claim value as a regexp, for the original ID token and the refreshed ID token. + // This function should return an empty string when there should be no username claim in the ID tokens. + wantDownstreamIDTokenUsernameToMatch func(username string) string + // The expected ID token groups claim value, for the original ID token and the refreshed ID token. + wantDownstreamIDTokenGroups []string + // The expected ID token additional claims, which will be nested under claim "additionalClaims", + // for the original ID token and the refreshed ID token. + wantDownstreamIDTokenAdditionalClaims map[string]interface{} + // The expected ID token lifetime, as calculated by token claim 'exp' subtracting token claim 'iat'. + // ID tokens issued through authcode exchange or token refresh should have the configured lifetime (or default if not configured). + // ID tokens issued through a token exchange should have the default lifetime. + wantDownstreamIDTokenLifetime *time.Duration + + // Want the authorization endpoint to redirect to the callback with this error type. + // The rest of the flow will be skipped since the initial authorization failed. + wantAuthorizationErrorType string + // Want the authorization endpoint to redirect to the callback with this error description. + // Should be used with wantAuthorizationErrorType. + wantAuthorizationErrorDescription string + + // Optionally want to the authcode exchange at the token endpoint to fail. The rest of the flow will be + // skipped since the authcode exchange failed. + wantAuthcodeExchangeError string + + // Optionally make all required assertions about the response of the RFC8693 token exchange for + // the cluster-scoped ID token, given the http response status and response body from the token endpoint. + // When this is not specified then the appropriate default assertions for a successful exchange are made. + // Even if this expects failures, the rest of the flow will continue. + wantTokenExchangeResponse func(t *testing.T, status int, body string) + + // Optionally edit the refresh session data between the initial login and the first refresh, + // which is still expected to succeed after these edits. Returns the group memberships expected after the + // refresh is performed. + editRefreshSessionDataWithoutBreaking func(t *testing.T, sessionData *psession.PinnipedSession, idpName, username string) []string + // Optionally either revoke the user's session on the upstream provider, or manipulate the user's session + // data in such a way that it should cause the next upstream refresh attempt to fail. + breakRefreshSessionData func(t *testing.T, sessionData *psession.PinnipedSession, idpName, username string) +} + func TestSupervisorLogin_Browser(t *testing.T) { env := testlib.IntegrationEnv(t) @@ -56,6 +163,16 @@ func TestSupervisorLogin_Browser(t *testing.T) { testlib.SkipTestWhenLDAPIsUnavailable(t, env) } + skipAnyGitHubTests := func(t *testing.T) { + t.Helper() + testlib.SkipTestWhenGitHubIsUnavailable(t) + } + + skipGitHubOAuthAppTestsButRunOtherGitHubTests := func(t *testing.T) { + t.Helper() + testlib.SkipTestWhenGitHubOAuthClientCallbackDoesNotMatchFederationDomainIssuerCallback(t) + } + skipActiveDirectoryTests := func(t *testing.T) { t.Helper() testlib.SkipTestWhenActiveDirectoryIsUnavailable(t, env) @@ -68,7 +185,7 @@ func TestSupervisorLogin_Browser(t *testing.T) { CertificateAuthorityData: base64.StdEncoding.EncodeToString([]byte(env.SupervisorUpstreamOIDC.CABundle)), }, Client: idpv1alpha1.OIDCClient{ - SecretName: testlib.CreateClientCredsSecret(t, env.SupervisorUpstreamOIDC.ClientID, env.SupervisorUpstreamOIDC.ClientSecret).Name, + SecretName: testlib.CreateOIDCClientCredentialsSecret(t, env.SupervisorUpstreamOIDC.ClientID, env.SupervisorUpstreamOIDC.ClientSecret).Name, }, } } @@ -205,112 +322,7 @@ func TestSupervisorLogin_Browser(t *testing.T) { regexp.QuoteMeta("&sub=") + ".+" + "$" - // These tests attempt to exercise the entire login and refresh flow of the Supervisor for various cases. - // They do not use the Pinniped CLI as the client, which allows them to exercise the Supervisor as an - // OIDC provider in ways that the CLI might not use. Similar tests exist using the CLI in e2e_test.go. - // - // Each of these tests perform the following flow: - // 1. Configure an IDP CR. - // 2. Create a FederationDomain with TLS configured and wait for its JWKS endpoint to be available. - // 3. Call the authorization endpoint and log in as a specific user. - // Note that these tests do not use form_post response type (which is tested by e2e_test.go). - // 4. Listen on a local callback server for the authorization redirect, and assert that it was success or failure. - // 5. Call the token endpoint to exchange the authcode. - // 6. Call the token endpoint to perform the RFC8693 token exchange for the cluster-scoped ID token. - // 7. Potentially edit the refresh session data or IDP settings before the refresh. - // 8. Call the token endpoint to perform a refresh, and expect it to succeed. - // 9. Call the token endpoint again to perform another RFC8693 token exchange for the cluster-scoped ID token, - // this time using the recently refreshed tokens when submitting the request. - // 10. Potentially edit the refresh session data or IDP settings again, this time in such a way that the next - // refresh should fail. If done, then perform one more refresh and expect failure. - tests := []struct { - name string - - // This required function might choose to skip the test case, for example if the LDAP server is not - // available for an LDAP test. - maybeSkip func(t *testing.T) - - // This required function should configure an IDP CR. It should also wait for it to be ready and schedule - // its cleanup. Return the name of the IDP CR. - createIDP func(t *testing.T) string - - // Optionally specify the identityProviders part of the FederationDomain's spec by returning it from this function. - // Also return the displayName of the IDP that should be used during authentication (or empty string for no IDP name in the auth request). - // This function takes the name of the IDP CR which was returned by createIDP() as as argument. - federationDomainIDPs func(t *testing.T, idpName string) (idps []supervisorconfigv1alpha1.FederationDomainIdentityProvider, useIDPDisplayName string) - - // Optionally create an OIDCClient CR for the test to use. Return the client ID and client secret for the - // test to use. When not set, the test will default to using the "pinniped-cli" static client with no secret. - // When a client secret is returned, it will be used for authcode exchange, refresh requests, and RFC8693 - // token exchanges for cluster-scoped tokens (client secrets are not needed in authorization requests). - createOIDCClient func(t *testing.T, callbackURL string) (string, string) - - // Optionally return the username and password for the test to use when logging in. This username/password - // will be passed to requestAuthorization(), or empty strings will be passed to indicate that the defaults - // should be used. If there is any cleanup required, then this function should also schedule that cleanup. - testUser func(t *testing.T) (string, string) - - // This required function should call the authorization endpoint using the given URL and also perform whatever - // interactions are needed to log in as the user. - requestAuthorization func(t *testing.T, downstreamIssuer, downstreamAuthorizeURL, downstreamCallbackURL, username, password string, httpClient *http.Client) - - // This string will be used as the requested audience in the RFC8693 token exchange for - // the cluster-scoped ID token. When it is not specified, a default string will be used. - requestTokenExchangeAud string - - // The scopes to request from the authorization endpoint. Defaults will be used when not specified. - downstreamScopes []string - // The scopes to want granted from the authorization endpoint. Defaults to the downstreamScopes value when not, - // specified, i.e. by default it expects that all requested scopes were granted. - wantDownstreamScopes []string - - // When we want the localhost callback to have never happened, then the flow will stop there. The login was - // unable to finish so there is nothing to assert about what should have happened with the callback, and there - // won't be any error sent to the callback either. This would happen, for example, when the user fails to log - // in at the LDAP/AD login page, because then they would be redirected back to that page again, instead of - // getting a callback success/error redirect. - wantLocalhostCallbackToNeverHappen bool - - // The expected ID token subject claim value as a regexp, for the original ID token and the refreshed ID token. - wantDownstreamIDTokenSubjectToMatch string - // The expected ID token username claim value as a regexp, for the original ID token and the refreshed ID token. - // This function should return an empty string when there should be no username claim in the ID tokens. - wantDownstreamIDTokenUsernameToMatch func(username string) string - // The expected ID token groups claim value, for the original ID token and the refreshed ID token. - wantDownstreamIDTokenGroups []string - // The expected ID token additional claims, which will be nested under claim "additionalClaims", - // for the original ID token and the refreshed ID token. - wantDownstreamIDTokenAdditionalClaims map[string]any - // The expected ID token lifetime, as calculated by token claim 'exp' subtracting token claim 'iat'. - // ID tokens issued through authcode exchange or token refresh should have the configured lifetime (or default if not configured). - // ID tokens issued through a token exchange should have the default lifetime. - wantDownstreamIDTokenLifetime *time.Duration - - // Want the authorization endpoint to redirect to the callback with this error type. - // The rest of the flow will be skipped since the initial authorization failed. - wantAuthorizationErrorType string - // Want the authorization endpoint to redirect to the callback with this error description. - // Should be used with wantAuthorizationErrorType. - wantAuthorizationErrorDescription string - - // Optionally want to the authcode exchange at the token endpoint to fail. The rest of the flow will be - // skipped since the authcode exchange failed. - wantAuthcodeExchangeError string - - // Optionally make all required assertions about the response of the RFC8693 token exchange for - // the cluster-scoped ID token, given the http response status and response body from the token endpoint. - // When this is not specified then the appropriate default assertions for a successful exchange are made. - // Even if this expects failures, the rest of the flow will continue. - wantTokenExchangeResponse func(t *testing.T, status int, body string) - - // Optionally edit the refresh session data between the initial login and the first refresh, - // which is still expected to succeed after these edits. Returns the group memberships expected after the - // refresh is performed. - editRefreshSessionDataWithoutBreaking func(t *testing.T, sessionData *psession.PinnipedSession, idpName, username string) []string - // Optionally either revoke the user's session on the upstream provider, or manipulate the user's session - // data in such a way that it should cause the next upstream refresh attempt to fail. - breakRefreshSessionData func(t *testing.T, sessionData *psession.PinnipedSession, idpName, username string) - }{ + tests := []*supervisorLoginTestcase{ { name: "oidc with default username and groups claim settings", maybeSkip: skipNever, @@ -2174,6 +2186,24 @@ func TestSupervisorLogin_Browser(t *testing.T) { }, } + // Append testcases for GitHub using a GitHub App as the client. + tests = append(tests, + supervisorLoginGithubTestcases(env, + env.SupervisorUpstreamGithub.GithubAppClientID, + env.SupervisorUpstreamGithub.GithubAppClientSecret, + "using GitHub App as client", + skipAnyGitHubTests)..., + ) + + // Append those same testcases for GitHub again, but this time using one of GitHub's old-style OAuth Apps as the client. + tests = append(tests, + supervisorLoginGithubTestcases(env, + env.SupervisorUpstreamGithub.GithubOAuthAppClientID, + env.SupervisorUpstreamGithub.GithubOAuthAppClientSecret, + "using old-style GitHub OAuth App as client", + skipGitHubOAuthAppTestsButRunOtherGitHubTests)..., + ) + for _, test := range tests { tt := test t.Run(tt.name, func(t *testing.T) { @@ -2206,7 +2236,163 @@ func TestSupervisorLogin_Browser(t *testing.T) { } } -func wantGroupsInAdditionalClaimsIfGroupsExist(additionalClaims map[string]any, wantGroupsAdditionalClaimName string, wantGroups []string) map[string]any { +func supervisorLoginGithubTestcases( + env *testlib.TestEnv, + clientID string, + clientSecret string, + nameNote string, + maybeSkip func(t *testing.T), +) []*supervisorLoginTestcase { + basicGitHubIdentityProviderSpec := func(t *testing.T, clientID, clientSecret string) idpv1alpha1.GitHubIdentityProviderSpec { + return idpv1alpha1.GitHubIdentityProviderSpec{ + AllowAuthentication: idpv1alpha1.GitHubAllowAuthenticationSpec{ + Organizations: idpv1alpha1.GitHubOrganizationsSpec{ + Policy: ptr.To(idpv1alpha1.GitHubAllowedAuthOrganizationsPolicyAllGitHubUsers), + }, + }, + Client: idpv1alpha1.GitHubClientSpec{ + SecretName: testlib.CreateGitHubClientCredentialsSecret(t, clientID, clientSecret).Name, + }, + } + } + + // The downstream ID token Subject should include the upstream user ID after the upstream issuer name + // and IDP display name. + expectedIDTokenSubjectRegexForUpstreamGitHub := "^" + + regexp.QuoteMeta("https://api.github.com?idpName=test-upstream-github-idp-") + `[\w]+` + + regexp.QuoteMeta("&login=") + regexp.QuoteMeta(env.SupervisorUpstreamGithub.TestUserUsername) + + regexp.QuoteMeta("&id=") + regexp.QuoteMeta(env.SupervisorUpstreamGithub.TestUserID) + + "$" + + return []*supervisorLoginTestcase{ + { + name: fmt.Sprintf("github with all orgs allowed and default claim settings (%s)", nameNote), + maybeSkip: maybeSkip, + createIDP: func(t *testing.T) string { + return testlib.CreateTestGitHubIdentityProvider(t, + basicGitHubIdentityProviderSpec(t, clientID, clientSecret), + idpv1alpha1.GitHubPhaseReady).Name + }, + requestAuthorization: requestAuthorizationUsingBrowserAuthcodeFlowGitHub, + editRefreshSessionDataWithoutBreaking: func(t *testing.T, sessionData *psession.PinnipedSession, _, _ string) []string { + // Even if we update this group to some names that did not come from the GitHub API, + // we expect that it will return to the real groups from the GitHub API after we refresh. + initialGroupMembership := []string{"some-wrong-group", "some-other-group"} + sessionData.Custom.UpstreamGroups = initialGroupMembership // upstream group names in session + sessionData.Fosite.Claims.Extra["groups"] = initialGroupMembership // downstream group names in session + return env.SupervisorUpstreamGithub.TestUserExpectedTeamSlugs + }, + breakRefreshSessionData: func(t *testing.T, pinnipedSession *psession.PinnipedSession, _, _ string) { + // Pretend that the github access token was revoked or expired by changing it to an + // invalid access token in the user's session data. This should cause refresh to fail because + // during the refresh the GitHub API will not accept this bad access token. + customSessionData := pinnipedSession.Custom + require.Equal(t, psession.ProviderTypeGitHub, customSessionData.ProviderType) + require.NotEmpty(t, customSessionData.GitHub.UpstreamAccessToken) + customSessionData.GitHub.UpstreamAccessToken = "purposely-using-bad-access-token-during-an-automated-integration-test" + }, + wantDownstreamIDTokenSubjectToMatch: expectedIDTokenSubjectRegexForUpstreamGitHub, + wantDownstreamIDTokenUsernameToMatch: func(_ string) string { + return "^" + regexp.QuoteMeta(env.SupervisorUpstreamGithub.TestUserUsername+":"+env.SupervisorUpstreamGithub.TestUserID) + "$" + }, + wantDownstreamIDTokenGroups: env.SupervisorUpstreamGithub.TestUserExpectedTeamSlugs, + }, + { + name: fmt.Sprintf("github with list of allowed orgs, username as login, and groups as names (%s)", nameNote), + maybeSkip: maybeSkip, + createIDP: func(t *testing.T) string { + spec := basicGitHubIdentityProviderSpec(t, clientID, clientSecret) + spec.AllowAuthentication = idpv1alpha1.GitHubAllowAuthenticationSpec{ + Organizations: idpv1alpha1.GitHubOrganizationsSpec{ + Policy: ptr.To(idpv1alpha1.GitHubAllowedAuthOrganizationsPolicyOnlyUsersFromAllowedOrganizations), + Allowed: []string{env.SupervisorUpstreamGithub.TestUserOrganization, "some-unrelated-org"}, + }, + } + spec.Claims = idpv1alpha1.GitHubClaims{ + Username: ptr.To(idpv1alpha1.GitHubUsernameLogin), + Groups: ptr.To(idpv1alpha1.GitHubUseTeamNameForGroupName), + } + return testlib.CreateTestGitHubIdentityProvider(t, spec, idpv1alpha1.GitHubPhaseReady).Name + }, + requestAuthorization: requestAuthorizationUsingBrowserAuthcodeFlowGitHub, + wantDownstreamIDTokenSubjectToMatch: expectedIDTokenSubjectRegexForUpstreamGitHub, + wantDownstreamIDTokenUsernameToMatch: func(_ string) string { + return "^" + regexp.QuoteMeta(env.SupervisorUpstreamGithub.TestUserUsername) + "$" + }, + wantDownstreamIDTokenGroups: env.SupervisorUpstreamGithub.TestUserExpectedTeamNames, + }, + { + name: fmt.Sprintf("github with list of allowed orgs differently cased, username as id, and groups as names (%s)", nameNote), + maybeSkip: maybeSkip, + createIDP: func(t *testing.T) string { + spec := basicGitHubIdentityProviderSpec(t, clientID, clientSecret) + spec.AllowAuthentication = idpv1alpha1.GitHubAllowAuthenticationSpec{ + Organizations: idpv1alpha1.GitHubOrganizationsSpec{ + Policy: ptr.To(idpv1alpha1.GitHubAllowedAuthOrganizationsPolicyOnlyUsersFromAllowedOrganizations), + Allowed: []string{strings.ToUpper(env.SupervisorUpstreamGithub.TestUserOrganization), "some-unrelated-org"}, + }, + } + spec.Claims = idpv1alpha1.GitHubClaims{ + Username: ptr.To(idpv1alpha1.GitHubUsernameID), + Groups: ptr.To(idpv1alpha1.GitHubUseTeamNameForGroupName), + } + return testlib.CreateTestGitHubIdentityProvider(t, spec, idpv1alpha1.GitHubPhaseReady).Name + }, + requestAuthorization: requestAuthorizationUsingBrowserAuthcodeFlowGitHub, + wantDownstreamIDTokenSubjectToMatch: expectedIDTokenSubjectRegexForUpstreamGitHub, + wantDownstreamIDTokenUsernameToMatch: func(_ string) string { + return "^" + regexp.QuoteMeta(env.SupervisorUpstreamGithub.TestUserID) + "$" + }, + wantDownstreamIDTokenGroups: env.SupervisorUpstreamGithub.TestUserExpectedTeamNames, + }, + { + name: fmt.Sprintf("github when user does not belong to any of the allowed orgs, should fail at the Supervisor callback endpoint (%s)", nameNote), + maybeSkip: maybeSkip, + createIDP: func(t *testing.T) string { + spec := basicGitHubIdentityProviderSpec(t, clientID, clientSecret) + spec.AllowAuthentication = idpv1alpha1.GitHubAllowAuthenticationSpec{ + Organizations: idpv1alpha1.GitHubOrganizationsSpec{ + Policy: ptr.To(idpv1alpha1.GitHubAllowedAuthOrganizationsPolicyOnlyUsersFromAllowedOrganizations), + Allowed: []string{"some-unrelated-org"}, + }, + } + return testlib.CreateTestGitHubIdentityProvider(t, spec, idpv1alpha1.GitHubPhaseReady).Name + }, + federationDomainIDPs: func(t *testing.T, idpName string) ([]configv1alpha1.FederationDomainIdentityProvider, string) { + displayName := "some-github-identity-provider-name" + return []configv1alpha1.FederationDomainIdentityProvider{ + { + DisplayName: displayName, + ObjectRef: corev1.TypedLocalObjectReference{ + APIGroup: ptr.To("idp.supervisor." + env.APIGroupSuffix), + Kind: "GitHubIdentityProvider", + Name: idpName, + }, + }, + }, + displayName + }, + requestAuthorization: func(t *testing.T, _, downstreamAuthorizeURL, downstreamCallbackURL, _, _ string, httpClient *http.Client) { + t.Helper() + browser := openBrowserAndNavigateToAuthorizeURL(t, downstreamAuthorizeURL, httpClient) + // Expect to be redirected to the upstream provider and log in. + browsertest.LoginToUpstreamGitHub(t, browser, env.SupervisorUpstreamGithub) + // Wait for the login to happen and us be redirected back to the Supervisor callback with an error showing. + t.Logf("waiting for redirect to Supervisor callback endpoint, which should be showing an error") + callbackURLPattern := regexp.MustCompile(`\A` + regexp.QuoteMeta(env.SupervisorUpstreamOIDC.CallbackURL) + `\?.+\z`) + browser.WaitForURL(t, callbackURLPattern) + // Get the text of the preformatted error message showing on the page. + textOfPreTag := browser.TextOfFirstMatch(t, "pre") + require.Equal(t, + `Forbidden: login denied due to configuration on GitHubIdentityProvider with display name "some-github-identity-provider-name": user is not allowed to log in due to organization membership policy`+"\n", + textOfPreTag) + }, + wantLocalhostCallbackToNeverHappen: true, + }, + } +} + +func wantGroupsInAdditionalClaimsIfGroupsExist(additionalClaims map[string]interface{}, wantGroupsAdditionalClaimName string, wantGroups []string) map[string]interface{} { if len(wantGroups) > 0 { var wantGroupsAnyType []any for _, group := range wantGroups { @@ -2360,7 +2546,7 @@ func testSupervisorLogin( ) { env := testlib.IntegrationEnv(t) - ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute) + ctx, cancel := context.WithTimeout(context.Background(), 7*time.Minute) defer cancel() // Infer the downstream issuer URL from the callback associated with the upstream test client registration. @@ -2427,7 +2613,7 @@ func testSupervisorLogin( } // Create the downstream FederationDomain and expect it to go into the appropriate status condition. - downstream := testlib.CreateTestFederationDomain(ctx, t, + federationDomain := testlib.CreateTestFederationDomain(ctx, t, supervisorconfigv1alpha1.FederationDomainSpec{ Issuer: issuerURL.String(), TLS: &supervisorconfigv1alpha1.FederationDomainTLSSpec{SecretName: certSecret.Name}, @@ -2476,7 +2662,7 @@ func testSupervisorLogin( var discovery *coreosoidc.Provider testlib.RequireEventually(t, func(requireEventually *require.Assertions) { var err error - discovery, err = coreosoidc.NewProvider(oidcHTTPClientContext, downstream.Spec.Issuer) + discovery, err = coreosoidc.NewProvider(oidcHTTPClientContext, federationDomain.Spec.Issuer) requireEventually.NoError(err) }, 30*time.Second, 200*time.Millisecond) @@ -2527,7 +2713,7 @@ func testSupervisorLogin( downstreamAuthorizeURL := downstreamOAuth2Config.AuthCodeURL(stateParam.String(), authorizeRequestParams...) // Perform parameterized auth code acquisition. - requestAuthorization(t, downstream.Spec.Issuer, downstreamAuthorizeURL, localCallbackServer.URL, username, password, httpClient) + requestAuthorization(t, federationDomain.Spec.Issuer, downstreamAuthorizeURL, localCallbackServer.URL, username, password, httpClient) // Expect that our callback handler was invoked. callback, err := localCallbackServer.waitForCallback(10 * time.Second) @@ -2718,6 +2904,7 @@ func testSupervisorLogin( // Should have got an error since the upstream refresh should have failed. require.Error(t, err) require.EqualError(t, err, `oauth2: "error" "Error during upstream refresh. Upstream refresh failed."`) + t.Log("successfully confirmed that breaking the refresh session data caused the refresh to fail") } } @@ -2884,6 +3071,19 @@ func loginToUpstreamOIDCAndWaitForCallback(t *testing.T, b *browsertest.Browser, b.WaitForURL(t, callbackURLPattern) } +func loginToUpstreamGitHubAndWaitForCallback(t *testing.T, b *browsertest.Browser, downstreamCallbackURL string) { + t.Helper() + env := testlib.IntegrationEnv(t) + + // Expect to be redirected to the upstream provider and log in. + browsertest.LoginToUpstreamGitHub(t, b, env.SupervisorUpstreamGithub) + + // Wait for the login to happen and us be redirected back to a localhost callback. + t.Logf("waiting for redirect to callback") + callbackURLPattern := regexp.MustCompile(`\A` + regexp.QuoteMeta(downstreamCallbackURL) + `\?.+\z`) + b.WaitForURL(t, callbackURLPattern) +} + func requestAuthorizationUsingBrowserAuthcodeFlowOIDC(t *testing.T, _, downstreamAuthorizeURL, downstreamCallbackURL, _, _ string, httpClient *http.Client) { t.Helper() @@ -2892,6 +3092,14 @@ func requestAuthorizationUsingBrowserAuthcodeFlowOIDC(t *testing.T, _, downstrea loginToUpstreamOIDCAndWaitForCallback(t, browser, downstreamCallbackURL) } +func requestAuthorizationUsingBrowserAuthcodeFlowGitHub(t *testing.T, _, downstreamAuthorizeURL, downstreamCallbackURL, _, _ string, httpClient *http.Client) { + t.Helper() + + browser := openBrowserAndNavigateToAuthorizeURL(t, downstreamAuthorizeURL, httpClient) + + loginToUpstreamGitHubAndWaitForCallback(t, browser, downstreamCallbackURL) +} + func requestAuthorizationUsingBrowserAuthcodeFlowOIDCWithIDPChooserPage(t *testing.T, downstreamIssuer, downstreamAuthorizeURL, downstreamCallbackURL, _, _ string, httpClient *http.Client) { t.Helper() diff --git a/test/integration/supervisor_storage_test.go b/test/integration/supervisor_storage_test.go index 8bee33c5f..0639a0881 100644 --- a/test/integration/supervisor_storage_test.go +++ b/test/integration/supervisor_storage_test.go @@ -93,7 +93,7 @@ func TestAuthorizeCodeStorage(t *testing.T) { // Note that CreateAuthorizeCodeSession() sets Active to true and also sets the Version before storing the session, // so expect those here. session.Active = true - session.Version = "7" // this is the value of the authorizationcode.authorizeCodeStorageVersion constant + session.Version = "8" // this is the value of the authorizationcode.authorizeCodeStorageVersion constant expectedSessionStorageJSON, err := json.Marshal(session) require.NoError(t, err) require.JSONEq(t, string(expectedSessionStorageJSON), string(initialSecret.Data["pinniped-storage-data"])) diff --git a/test/integration/supervisor_upstream_test.go b/test/integration/supervisor_upstream_test.go index 7cb21ce84..0c7e463d7 100644 --- a/test/integration/supervisor_upstream_test.go +++ b/test/integration/supervisor_upstream_test.go @@ -28,7 +28,7 @@ func TestSupervisorUpstreamOIDCDiscovery(t *testing.T) { upstream := testlib.CreateTestOIDCIdentityProvider(t, spec, idpv1alpha1.PhaseError) expectUpstreamConditions(t, upstream, []metav1.Condition{ { - Type: "ClientCredentialsValid", + Type: "ClientCredentialsSecretValid", Status: metav1.ConditionFalse, Reason: "SecretNotFound", Message: `secret "does-not-exist" not found`, @@ -60,13 +60,13 @@ Get "https://127.0.0.1:444444/invalid-url-that-is-really-really-long-nananananan AdditionalScopes: []string{"email", "profile"}, }, Client: idpv1alpha1.OIDCClient{ - SecretName: testlib.CreateClientCredsSecret(t, "test-client-id", "test-client-secret").Name, + SecretName: testlib.CreateOIDCClientCredentialsSecret(t, "test-client-id", "test-client-secret").Name, }, } upstream := testlib.CreateTestOIDCIdentityProvider(t, spec, idpv1alpha1.PhaseError) expectUpstreamConditions(t, upstream, []metav1.Condition{ { - Type: "ClientCredentialsValid", + Type: "ClientCredentialsSecretValid", Status: metav1.ConditionTrue, Reason: "Success", Message: "loaded client credentials", @@ -98,13 +98,13 @@ oidc: issuer did not match the issuer returned by provider, expected "` + env.Su AdditionalScopes: []string{"email", "profile"}, }, Client: idpv1alpha1.OIDCClient{ - SecretName: testlib.CreateClientCredsSecret(t, "test-client-id", "test-client-secret").Name, + SecretName: testlib.CreateOIDCClientCredentialsSecret(t, "test-client-id", "test-client-secret").Name, }, } upstream := testlib.CreateTestOIDCIdentityProvider(t, spec, idpv1alpha1.PhaseReady) expectUpstreamConditions(t, upstream, []metav1.Condition{ { - Type: "ClientCredentialsValid", + Type: "ClientCredentialsSecretValid", Status: metav1.ConditionTrue, Reason: "Success", Message: "loaded client credentials", diff --git a/test/integration/supervisor_warnings_test.go b/test/integration/supervisor_warnings_test.go index cdf7fbb8a..17da03367 100644 --- a/test/integration/supervisor_warnings_test.go +++ b/test/integration/supervisor_warnings_test.go @@ -418,7 +418,7 @@ func TestSupervisorWarnings_Browser(t *testing.T) { Groups: env.SupervisorUpstreamOIDC.GroupsClaim, }, Client: idpv1alpha1.OIDCClient{ - SecretName: testlib.CreateClientCredsSecret(t, env.SupervisorUpstreamOIDC.ClientID, env.SupervisorUpstreamOIDC.ClientSecret).Name, + SecretName: testlib.CreateOIDCClientCredentialsSecret(t, env.SupervisorUpstreamOIDC.ClientID, env.SupervisorUpstreamOIDC.ClientSecret).Name, }, }, idpv1alpha1.PhaseReady) testlib.WaitForFederationDomainStatusPhase(ctx, t, downstream.Name, supervisorconfigv1alpha1.FederationDomainPhaseReady) diff --git a/test/testlib/browsertest/browsertest.go b/test/testlib/browsertest/browsertest.go index 5c2a93a95..1600a9c36 100644 --- a/test/testlib/browsertest/browsertest.go +++ b/test/testlib/browsertest/browsertest.go @@ -5,7 +5,10 @@ package browsertest import ( + "bytes" + "compress/gzip" "context" + "encoding/base64" "fmt" "log" "regexp" @@ -16,10 +19,12 @@ import ( "time" chromedpbrowser "github.com/chromedp/cdproto/browser" + "github.com/chromedp/cdproto/dom" chromedpruntime "github.com/chromedp/cdproto/runtime" "github.com/chromedp/chromedp" "github.com/stretchr/testify/require" + "go.pinniped.dev/internal/testutil/totp" "go.pinniped.dev/test/testlib" ) @@ -162,12 +167,52 @@ func OpenBrowser(t *testing.T) *Browser { for _, e := range b.exceptionEvents { t.Logf("exception: %s", e) } + + // If the test failed, dump helpful debugging info from the browser's final page. + if t.Failed() { + b.dumpPage(t) + } }) // Done. The browser is ready to be driven by the test. return b } +func (b *Browser) dumpPage(t *testing.T) { + // Log the URL of the current page. + var url string + b.runWithTimeout(t, b.timeout(), chromedp.Location(&url)) + t.Logf("Browser URL from end of test %q: %s", t.Name(), url) + + // Log the title of the current page. + t.Logf("Browser page title from end of test %q: %q", t.Name(), b.Title(t)) + + // Log a screenshot of the current page. + var screenBuf []byte + b.runWithTimeout(t, b.timeout(), chromedp.FullScreenshot(&screenBuf, 10)) // low quality to make it smaller + t.Logf("Browser screenshot (base64 encoded jpeg format) from end of test %q:\n%s\n", + t.Name(), base64.StdEncoding.EncodeToString(screenBuf)) + + // Log the HTML of the current page. + var html string + b.runWithTimeout(t, b.timeout(), chromedp.ActionFunc(func(ctx context.Context) error { + node, err := dom.GetDocument().Do(ctx) + if err != nil { + return err + } + html, err = dom.GetOuterHTML().WithNodeID(node.NodeID).Do(ctx) + return err + })) + var htmlBuf bytes.Buffer + gz := gzip.NewWriter(&htmlBuf) + _, err := gz.Write([]byte(html)) + require.NoError(t, err) + err = gz.Close() + require.NoError(t, err) + t.Logf("Browser html (gzip and base64 encoded) from end of test %q:\n%s\n", + t.Name(), base64.StdEncoding.EncodeToString(htmlBuf.Bytes())) +} + func (b *Browser) timeout() time.Duration { return 30 * time.Second } @@ -357,6 +402,154 @@ func LoginToUpstreamOIDC(t *testing.T, b *Browser, upstream testlib.TestOIDCUpst b.ClickFirstMatch(t, cfg.LoginButtonSelector) } +// LoginToUpstreamGitHub expects the page to be redirected to GitHub. +// It knows how to enter the test username/password and submit the upstream login form. +func LoginToUpstreamGitHub(t *testing.T, b *Browser, upstream testlib.TestGithubUpstream) { + t.Helper() + + // Expect to be redirected to the login page. + t.Logf("waiting for redirect to GitHub login page") + b.WaitForURL(t, regexp.MustCompile(`\Ahttps://github\.com/login.+\z`)) + + usernameSelector := "input#login_field" + passwordSelector := "input#password" + loginButtonSelector := "input[type=submit]" + + // Wait for the login page to be rendered. + b.WaitForVisibleElements(t, usernameSelector, passwordSelector, loginButtonSelector) + + // Fill in the username and password and click "submit". + t.Logf("logging into GitHub") + b.SendKeysToFirstMatch(t, usernameSelector, upstream.TestUserUsername) + b.SendKeysToFirstMatch(t, passwordSelector, upstream.TestUserPassword) + b.ClickFirstMatch(t, loginButtonSelector) + + handleGithubOTPLoginPage(t, b, upstream) + + // Keep looping until we get to a page that we do not know how to handle. Then return to allow the test to move on. + for handleOccasionalGithubLoginPage(t, b, upstream) { + continue + } +} + +func handleGithubOTPLoginPage(t *testing.T, b *Browser, upstream testlib.TestGithubUpstream) { + // Next, GitHub should go to a new page and prompt for the six digit MFA/OTP code. + otpSelector := "input#app_totp" + + // Wait for the MFA page to be rendered. + t.Logf("waiting for GitHub MFA page") + b.WaitForVisibleElements(t, otpSelector) + + // Sleep for a bit to make it less likely that we use the same OTP code twice when multiple tests are run in serial. + // GitHub gets upset when the same OTP code gets reused. + // GitHub seems to also get upset when any OTP codes are used often, like when all our GitHub tests run sequentially, + // because sometimes auth will go to a GitHub page that says: "We were unable to authenticate your request because too + // many codes have been submitted. Please wait a few minutes and contact support if you continue to have problems." + otpSleepSeconds := 60 + t.Logf("sleeping %d seconds before generating a GitHub OTP code", otpSleepSeconds) + time.Sleep(time.Duration(otpSleepSeconds) * time.Second) + + code, codeRemainingLifetimeSeconds := totp.GenerateOTPCode(t, upstream.TestUserOTPSecret, time.Now()) + if codeRemainingLifetimeSeconds < 2 { + t.Log("sleeping for 2 seconds before generating another OTP code") + time.Sleep(2 * time.Second) + code, _ = totp.GenerateOTPCode(t, upstream.TestUserOTPSecret, time.Now()) + } + + // Fill in the OTP code. We do not need to click "verify" because entering the code automatically submits the page. + t.Logf("entering GitHub OTP code") + b.SendKeysToFirstMatch(t, otpSelector, code) +} + +// handleOccasionalGithubLoginPage handles the interstitial pages which GitHub might show during a login flow. +// None of these will always happen. +func handleOccasionalGithubLoginPage(t *testing.T, b *Browser, upstream testlib.TestGithubUpstream) bool { + t.Helper() + + t.Log("sleeping for 2 seconds before looking at page title") + time.Sleep(2 * time.Second) + pageTitle := b.Title(t) + t.Logf("saw page title %q", pageTitle) + lowercaseTitle := strings.ToLower(pageTitle) + + switch { + case strings.HasPrefix(lowercaseTitle, "authorize "): // the title is "Authorize " + // Next GitHub might go to another page asking if you authorize the GitHub App to act on your behalf, + // if this user has never authorized this app. + // Wait for the authorize app page to be rendered. + t.Logf("waiting for GitHub authorize button") + // There are unfortunately two very similar buttons on this page: + // + submitConfirmButtonSelector := "button.btn-primary" + b.WaitForVisibleElements(t, submitConfirmButtonSelector) + t.Logf("clicking confirm button") + b.ClickFirstMatch(t, submitConfirmButtonSelector) + return true + + case strings.HasPrefix(lowercaseTitle, "verify two-factor authentication"): + // Next GitHub might occasionally as you to confirm your MFA settings. + // Wait for the page to be rendered. + t.Logf("waiting for GitHub skip link") + // There are several buttons and links. We want to click this link to "skip 2FA verification": + //