diff --git a/apis/supervisor/idp/v1alpha1/doc.go.tmpl b/apis/supervisor/idp/v1alpha1/doc.go.tmpl new file mode 100644 index 00000000..71f2737d --- /dev/null +++ b/apis/supervisor/idp/v1alpha1/doc.go.tmpl @@ -0,0 +1,11 @@ +// Copyright 2020 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +// +k8s:openapi-gen=true +// +k8s:deepcopy-gen=package +// +k8s:defaulter-gen=TypeMeta +// +groupName=idp.supervisor.pinniped.dev +// +groupGoName=IDP + +// Package v1alpha1 is the v1alpha1 version of the Pinniped supervisor identity provider (IDP) API. +package v1alpha1 diff --git a/apis/supervisor/idp/v1alpha1/register.go.tmpl b/apis/supervisor/idp/v1alpha1/register.go.tmpl new file mode 100644 index 00000000..67e549f9 --- /dev/null +++ b/apis/supervisor/idp/v1alpha1/register.go.tmpl @@ -0,0 +1,43 @@ +// Copyright 2020 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package v1alpha1 + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" +) + +const GroupName = "idp.supervisor.pinniped.dev" + +// SchemeGroupVersion is group version used to register these objects. +var SchemeGroupVersion = schema.GroupVersion{Group: GroupName, Version: "v1alpha1"} + +var ( + SchemeBuilder runtime.SchemeBuilder + localSchemeBuilder = &SchemeBuilder + AddToScheme = localSchemeBuilder.AddToScheme +) + +func init() { + // We only register manually written functions here. The registration of the + // generated functions takes place in the generated files. The separation + // makes the code compile even when the generated files are missing. + localSchemeBuilder.Register(addKnownTypes) +} + +// Adds the list of known types to the given scheme. +func addKnownTypes(scheme *runtime.Scheme) error { + scheme.AddKnownTypes(SchemeGroupVersion, + &UpstreamOIDCProvider{}, + &UpstreamOIDCProviderList{}, + ) + metav1.AddToGroupVersion(scheme, SchemeGroupVersion) + return nil +} + +// Resource takes an unqualified resource and returns a Group qualified GroupResource. +func Resource(resource string) schema.GroupResource { + return SchemeGroupVersion.WithResource(resource).GroupResource() +} diff --git a/apis/supervisor/idp/v1alpha1/types_meta.go.tmpl b/apis/supervisor/idp/v1alpha1/types_meta.go.tmpl new file mode 100644 index 00000000..e59976ff --- /dev/null +++ b/apis/supervisor/idp/v1alpha1/types_meta.go.tmpl @@ -0,0 +1,75 @@ +// Copyright 2020 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package v1alpha1 + +import metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + +// ConditionStatus is effectively an enum type for Condition.Status. +type ConditionStatus string + +// These are valid condition statuses. "ConditionTrue" means a resource is in the condition. +// "ConditionFalse" means a resource is not in the condition. "ConditionUnknown" means kubernetes +// can't decide if a resource is in the condition or not. In the future, we could add other +// intermediate conditions, e.g. ConditionDegraded. +const ( + ConditionTrue ConditionStatus = "True" + ConditionFalse ConditionStatus = "False" + ConditionUnknown ConditionStatus = "Unknown" +) + +// Condition status of a resource (mirrored from the metav1.Condition type added in Kubernetes 1.19). In a future API +// version we can switch to using the upstream type. +// See https://github.com/kubernetes/apimachinery/blob/v0.19.0/pkg/apis/meta/v1/types.go#L1353-L1413. +type Condition struct { + // 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) + // +required + // +kubebuilder:validation:Required + // +kubebuilder:validation: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])$` + // +kubebuilder:validation:MaxLength=316 + Type string `json:"type"` + + // status of the condition, one of True, False, Unknown. + // +required + // +kubebuilder:validation:Required + // +kubebuilder:validation:Enum=True;False;Unknown + Status ConditionStatus `json:"status"` + + // 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. + // +optional + // +kubebuilder:validation:Minimum=0 + ObservedGeneration int64 `json:"observedGeneration,omitempty"` + + // 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. + // +required + // +kubebuilder:validation:Required + // +kubebuilder:validation:Type=string + // +kubebuilder:validation:Format=date-time + LastTransitionTime metav1.Time `json:"lastTransitionTime"` + + // 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. + // +required + // +kubebuilder:validation:Required + // +kubebuilder:validation:MaxLength=1024 + // +kubebuilder:validation:MinLength=1 + // +kubebuilder:validation:Pattern=`^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$` + Reason string `json:"reason"` + + // message is a human readable message indicating details about the transition. + // This may be an empty string. + // +required + // +kubebuilder:validation:Required + // +kubebuilder:validation:MaxLength=32768 + Message string `json:"message"` +} diff --git a/apis/supervisor/idp/v1alpha1/types_upstreamoidcprovider.go.tmpl b/apis/supervisor/idp/v1alpha1/types_upstreamoidcprovider.go.tmpl new file mode 100644 index 00000000..7a16991b --- /dev/null +++ b/apis/supervisor/idp/v1alpha1/types_upstreamoidcprovider.go.tmpl @@ -0,0 +1,114 @@ +// Copyright 2020 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package v1alpha1 + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +type UpstreamOIDCProviderPhase string + +const ( + // PhasePending is the default phase for newly-created UpstreamOIDCProvider resources. + PhasePending UpstreamOIDCProviderPhase = "Pending" + + // PhaseReady is the phase for an UpstreamOIDCProvider resource in a healthy state. + PhaseReady UpstreamOIDCProviderPhase = "Ready" + + // PhaseErorr is the phase for an UpstreamOIDCProvider in an unhealthy state. + PhaseError UpstreamOIDCProviderPhase = "Error" +) + +// Status of an OIDC identity provider. +type UpstreamOIDCProviderStatus struct { + // Phase summarizes the overall status of the UpstreamOIDCProvider. + // +kubebuilder:default=Pending + // +kubebuilder:validation:Enum=Pending;Ready;Error + Phase UpstreamOIDCProviderPhase `json:"phase,omitempty"` + + // Represents the observations of an identity provider's current state. + // +patchMergeKey=type + // +patchStrategy=merge + // +listType=map + // +listMapKey=type + Conditions []Condition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type"` +} + +// OIDCAuthorizationConfig provides information about how to form the OAuth2 authorization +// request parameters. +type OIDCAuthorizationConfig struct { + // AdditionalScopes are the scopes in addition to "openid" that will be requested as part of the authorization + // request flow with an OIDC identity provider. By default only the "openid" scope will be requested. + AdditionalScopes []string `json:"additionalScopes"` +} + +// OIDCClaims provides a mapping from upstream claims into identities. +type OIDCClaims struct { + // Groups provides the name of the token claim that will be used to ascertain the groups to which + // an identity belongs. + Groups string `json:"groups"` + + // Username provides the name of the token claim that will be used to ascertain an identity's + // username. + Username string `json:"username"` +} + +// OIDCClient contains information about an OIDC client (e.g., client ID and client +// secret). +type OIDCClient struct { + // SecretName contains the name of a namespace-local Secret object that provides the clientID and + // clientSecret for an OIDC client. If only the SecretName is specified in an OIDCClient + // struct, then it is expected that the Secret is of type "secrets.pinniped.dev/oidc" with keys + // "clientID" and "clientSecret". + SecretName string `json:"secretName"` +} + +// Spec for configuring an OIDC identity provider. +type UpstreamOIDCProviderSpec struct { + // Issuer is the issuer URL of this OIDC identity provider, i.e., where to fetch + // /.well-known/openid-configuration. + // +kubebuilder:validation:MinLength=1 + // +kubebuilder:validation:Pattern=`^https://` + Issuer string `json:"issuer"` + + // AuthorizationConfig holds information about how to form the OAuth2 authorization request + // parameters to be used with this OIDC identity provider. + AuthorizationConfig OIDCAuthorizationConfig `json:"authorizationConfig"` + + // Claims provides the names of token claims that will be used when inspecting an identity from + // this OIDC identity provider. + Claims OIDCClaims `json:"claims"` + + // OIDCClient contains OIDC client information to be used used with this OIDC identity + // provider. + Client OIDCClient `json:"client"` +} + +// UpstreamOIDCProvider describes the configuration of an upstream OpenID Connect identity provider. +// +genclient +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object +// +kubebuilder:resource:categories=pinniped;pinniped-idp;pinniped-idps +// +kubebuilder:printcolumn:name="Issuer",type=string,JSONPath=`.spec.issuer` +// +kubebuilder:printcolumn:name="Status",type=string,JSONPath=`.status.phase` +// +kubebuilder:printcolumn:name="Age",type=date,JSONPath=`.metadata.creationTimestamp` +// +kubebuilder:subresource:status +type UpstreamOIDCProvider struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + // Spec for configuring the identity provider. + Spec UpstreamOIDCProviderSpec `json:"spec"` + + // Status of the identity provider. + Status UpstreamOIDCProviderStatus `json:"status,omitempty"` +} + +// List of UpstreamOIDCProvider objects. +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object +type UpstreamOIDCProviderList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + + Items []UpstreamOIDCProvider `json:"items"` +} diff --git a/deploy/supervisor/rbac.yaml b/deploy/supervisor/rbac.yaml index f260547d..33e86585 100644 --- a/deploy/supervisor/rbac.yaml +++ b/deploy/supervisor/rbac.yaml @@ -19,6 +19,12 @@ rules: - apiGroups: [config.supervisor.pinniped.dev] resources: [oidcproviders] verbs: [update, get, list, watch] + - apiGroups: [idp.supervisor.pinniped.dev] + resources: [upstreamoidcproviders] + verbs: [get, list, watch] + - apiGroups: [idp.supervisor.pinniped.dev] + resources: [upstreamoidcproviders/status] + verbs: [get, patch, update] --- kind: RoleBinding apiVersion: rbac.authorization.k8s.io/v1 diff --git a/deploy/supervisor/z0_crd_overlay.yaml b/deploy/supervisor/z0_crd_overlay.yaml index 6269da1f..b51556c5 100644 --- a/deploy/supervisor/z0_crd_overlay.yaml +++ b/deploy/supervisor/z0_crd_overlay.yaml @@ -9,3 +9,9 @@ metadata: #@overlay/match missing_ok=True labels: #@ labels() + +#@overlay/match by=overlay.subset({"kind": "CustomResourceDefinition", "metadata":{"name":"upstreamoidcproviders.idp.supervisor.pinniped.dev"}}), expects=1 +--- +metadata: + #@overlay/match missing_ok=True + labels: #@ labels() diff --git a/hack/lib/tilt/Tiltfile b/hack/lib/tilt/Tiltfile index 9358451d..cc4d44d5 100644 --- a/hack/lib/tilt/Tiltfile +++ b/hack/lib/tilt/Tiltfile @@ -113,6 +113,7 @@ k8s_resource( objects=[ # these are the objects that would otherwise appear in the "uncategorized" tab in the tilt UI 'oidcproviders.config.supervisor.pinniped.dev:customresourcedefinition', + 'upstreamoidcproviders.idp.supervisor.pinniped.dev:customresourcedefinition', 'pinniped-supervisor-static-config:configmap', 'supervisor:namespace', 'pinniped-supervisor:role', diff --git a/hack/lib/update-codegen.sh b/hack/lib/update-codegen.sh index fe1ab3c9..d4a103ac 100755 --- a/hack/lib/update-codegen.sh +++ b/hack/lib/update-codegen.sh @@ -112,7 +112,7 @@ echo "generating API-related code for our public API groups..." deepcopy \ "${BASE_PKG}/generated/${KUBE_MINOR_VERSION}/apis" \ "${BASE_PKG}/generated/${KUBE_MINOR_VERSION}/apis" \ - "supervisor/config:v1alpha1 concierge/config:v1alpha1 concierge/authentication:v1alpha1 concierge/login:v1alpha1" \ + "supervisor/config:v1alpha1 supervisor/idp:v1alpha1 concierge/config:v1alpha1 concierge/authentication:v1alpha1 concierge/login:v1alpha1" \ --go-header-file "${ROOT}/hack/boilerplate.go.txt" 2>&1 | sed "s|^|gen-api > |" ) @@ -148,7 +148,7 @@ echo "generating client code for our public API groups..." client,lister,informer \ "${BASE_PKG}/generated/${KUBE_MINOR_VERSION}/client/supervisor" \ "${BASE_PKG}/generated/${KUBE_MINOR_VERSION}/apis" \ - "supervisor/config:v1alpha1" \ + "supervisor/config:v1alpha1 supervisor/idp:v1alpha1" \ --go-header-file "${ROOT}/hack/boilerplate.go.txt" 2>&1 | sed "s|^|gen-client > |" ) @@ -168,6 +168,7 @@ crd-ref-docs \ # Generate CRD YAML (cd apis && controller-gen paths=./supervisor/config/v1alpha1 crd:trivialVersions=true output:crd:artifacts:config=../crds && + controller-gen paths=./supervisor/idp/v1alpha1 crd:trivialVersions=true output:crd:artifacts:config=../crds && controller-gen paths=./concierge/config/v1alpha1 crd:trivialVersions=true output:crd:artifacts:config=../crds && controller-gen paths=./concierge/authentication/v1alpha1 crd:trivialVersions=true output:crd:artifacts:config=../crds )