From b9b0365cd17bea51b256e42afedc8a8fa5060564 Mon Sep 17 00:00:00 2001 From: Andrew Azores Date: Fri, 3 May 2024 09:25:30 -0400 Subject: [PATCH] feat(auth): add config option to deploy oauth2_proxy instead of openshift-oauth-proxy (#803) --- api/v1beta2/cryostat_types.go | 23 +- api/v1beta2/zz_generated.deepcopy.go | 31 +++ ...yostat-operator.clusterserviceversion.yaml | 25 +- .../operator.cryostat.io_cryostats.yaml | 60 ++++- .../bases/operator.cryostat.io_cryostats.yaml | 60 ++++- ...yostat-operator.clusterserviceversion.yaml | 23 +- .../resource_definitions.go | 244 +++++++++++++++--- internal/controllers/configmaps.go | 95 +++++++ internal/controllers/reconciler.go | 10 +- internal/controllers/reconciler_test.go | 2 +- internal/controllers/secrets.go | 24 ++ internal/test/resources.go | 21 ++ 12 files changed, 579 insertions(+), 39 deletions(-) diff --git a/api/v1beta2/cryostat_types.go b/api/v1beta2/cryostat_types.go index 7689fa659..f66ed2b0d 100644 --- a/api/v1beta2/cryostat_types.go +++ b/api/v1beta2/cryostat_types.go @@ -15,6 +15,7 @@ package v1beta2 import ( + authzv1 "k8s.io/api/authorization/v1" corev1 "k8s.io/api/core/v1" netv1 "k8s.io/api/networking/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -487,12 +488,32 @@ type TemplateConfigMap struct { // Authorization options provide additional configurations for the auth proxy. type AuthorizationOptions struct { - // Reference to a secret and file name containing the Basic authentication htpasswd file + // Configuration for OpenShift RBAC to define which OpenShift user accounts may access the Cryostat application. + // +optional + // +operator-sdk:csv:customresourcedefinitions:type=spec,displayName="OpenShift SSO" + OpenShiftSSO *OpenShiftSSOConfig `json:"openShiftSSO,omitempty"` + // Reference to a secret and file name containing the Basic authentication htpasswd file. If deploying on OpenShift this + // defines additional user accounts that can access the Cryostat application, on top of the OpenShift user accounts which + // pass the OpenShift SSO Roles checks. If not on OpenShift then this defines the only user accounts that have access. // +optional // +operator-sdk:csv:customresourcedefinitions:type=spec,xDescriptors={"urn:alm:descriptor:io.kubernetes:Secret"} BasicAuth *SecretFile `json:"basicAuth,omitempty"` } +type OpenShiftSSOConfig struct { + // Disable OpenShift SSO integration and allow all users to access the application without authentication. This + // will also bypass the BasicAuth, if specified. + // +optional + // +operator-sdk:csv:customresourcedefinitions:type=spec,displayName="Disable OpenShift SSO",xDescriptors={"urn:alm:descriptor:com.tectonic.ui:booleanSwitch"} + Disable *bool `json:"disable,omitempty"` + // The SubjectAccessReview or TokenAccessReview that all clients (users visiting the application via web browser as well + // as CLI utilities and other programs presenting Bearer auth tokens) must pass in order to access the application. + // If not specified, the default role required is "create pods/exec" in the Cryostat application's installation namespace. + // +optional + // +operator-sdk:csv:customresourcedefinitions:type=spec + AccessReview *authzv1.ResourceAttributes `json:"accessReview,omitempty"` +} + type SecretFile struct { // Name of the secret to reference. // +optional diff --git a/api/v1beta2/zz_generated.deepcopy.go b/api/v1beta2/zz_generated.deepcopy.go index 050a5ff88..929146417 100644 --- a/api/v1beta2/zz_generated.deepcopy.go +++ b/api/v1beta2/zz_generated.deepcopy.go @@ -20,6 +20,7 @@ package v1beta2 import ( + authorizationv1 "k8s.io/api/authorization/v1" corev1 "k8s.io/api/core/v1" networkingv1 "k8s.io/api/networking/v1" "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -59,6 +60,11 @@ func (in *Affinity) DeepCopy() *Affinity { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *AuthorizationOptions) DeepCopyInto(out *AuthorizationOptions) { *out = *in + if in.OpenShiftSSO != nil { + in, out := &in.OpenShiftSSO, &out.OpenShiftSSO + *out = new(OpenShiftSSOConfig) + (*in).DeepCopyInto(*out) + } if in.BasicAuth != nil { in, out := &in.BasicAuth, &out.BasicAuth *out = new(SecretFile) @@ -455,6 +461,31 @@ func (in *NetworkConfigurationList) DeepCopy() *NetworkConfigurationList { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *OpenShiftSSOConfig) DeepCopyInto(out *OpenShiftSSOConfig) { + *out = *in + if in.Disable != nil { + in, out := &in.Disable, &out.Disable + *out = new(bool) + **out = **in + } + if in.AccessReview != nil { + in, out := &in.AccessReview, &out.AccessReview + *out = new(authorizationv1.ResourceAttributes) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OpenShiftSSOConfig. +func (in *OpenShiftSSOConfig) DeepCopy() *OpenShiftSSOConfig { + if in == nil { + return nil + } + out := new(OpenShiftSSOConfig) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *OperandMetadata) DeepCopyInto(out *OperandMetadata) { *out = *in diff --git a/bundle/manifests/cryostat-operator.clusterserviceversion.yaml b/bundle/manifests/cryostat-operator.clusterserviceversion.yaml index 2594ec1ea..18202da0e 100644 --- a/bundle/manifests/cryostat-operator.clusterserviceversion.yaml +++ b/bundle/manifests/cryostat-operator.clusterserviceversion.yaml @@ -53,7 +53,7 @@ metadata: capabilities: Seamless Upgrades categories: Monitoring, Developer Tools containerImage: quay.io/cryostat/cryostat-operator:3.0.0-dev - createdAt: "2024-04-26T21:51:07Z" + createdAt: "2024-05-03T13:22:52Z" description: JVM monitoring and profiling tool operatorframework.io/initialization-resource: |- { @@ -559,7 +559,10 @@ spec: x-descriptors: - urn:alm:descriptor:com.tectonic.ui:advanced - description: Reference to a secret and file name containing the Basic authentication - htpasswd file + htpasswd file. If deploying on OpenShift this defines additional user accounts + that can access the Cryostat application, on top of the OpenShift user accounts + which pass the OpenShift SSO Roles checks. If not on OpenShift then this + defines the only user accounts that have access. displayName: Basic Auth path: authorizationOptions.basicAuth x-descriptors: @@ -574,6 +577,24 @@ spec: path: authorizationOptions.basicAuth.secretName x-descriptors: - urn:alm:descriptor:io.kubernetes:Secret + - description: Configuration for OpenShift RBAC to define which OpenShift user + accounts may access the Cryostat application. + displayName: OpenShift SSO + path: authorizationOptions.openShiftSSO + - description: The SubjectAccessReview or TokenAccessReview that all clients + (users visiting the application via web browser as well as CLI utilities + and other programs presenting Bearer auth tokens) must pass in order to + access the application. If not specified, the default role required is "create + pods/exec" in the Cryostat application's installation namespace. + displayName: Access Review + path: authorizationOptions.openShiftSSO.accessReview + - description: Disable OpenShift SSO integration and allow all users to access + the application without authentication. This will also bypass the BasicAuth, + if specified. + displayName: Disable OpenShift SSO + path: authorizationOptions.openShiftSSO.disable + x-descriptors: + - urn:alm:descriptor:com.tectonic.ui:booleanSwitch - description: List of Flight Recorder Event Templates to preconfigure in Cryostat. displayName: Event Templates path: eventTemplates diff --git a/bundle/manifests/operator.cryostat.io_cryostats.yaml b/bundle/manifests/operator.cryostat.io_cryostats.yaml index dabd5b441..460244d0c 100644 --- a/bundle/manifests/operator.cryostat.io_cryostats.yaml +++ b/bundle/manifests/operator.cryostat.io_cryostats.yaml @@ -5156,7 +5156,11 @@ spec: properties: basicAuth: description: Reference to a secret and file name containing the - Basic authentication htpasswd file + Basic authentication htpasswd file. If deploying on OpenShift + this defines additional user accounts that can access the Cryostat + application, on top of the OpenShift user accounts which pass + the OpenShift SSO Roles checks. If not on OpenShift then this + defines the only user accounts that have access. properties: filename: description: Name of the file within the secret. @@ -5165,6 +5169,60 @@ spec: description: Name of the secret to reference. type: string type: object + openShiftSSO: + description: Configuration for OpenShift RBAC to define which + OpenShift user accounts may access the Cryostat application. + properties: + accessReview: + description: The SubjectAccessReview or TokenAccessReview + that all clients (users visiting the application via web + browser as well as CLI utilities and other programs presenting + Bearer auth tokens) must pass in order to access the application. + If not specified, the default role required is "create pods/exec" + in the Cryostat application's installation namespace. + properties: + group: + description: Group is the API Group of the Resource. "*" + means all. + type: string + name: + description: Name is the name of the resource being requested + for a "get" or deleted for a "delete". "" (empty) means + all. + type: string + namespace: + description: Namespace is the namespace of the action + being requested. Currently, there is no distinction + between no namespace and all namespaces "" (empty) is + defaulted for LocalSubjectAccessReviews "" (empty) is + empty for cluster-scoped resources "" (empty) means + "all" for namespace scoped resources from a SubjectAccessReview + or SelfSubjectAccessReview + type: string + resource: + description: Resource is one of the existing resource + types. "*" means all. + type: string + subresource: + description: Subresource is one of the existing resource + types. "" means none. + type: string + verb: + description: 'Verb is a kubernetes resource API verb, + like: get, list, watch, create, update, delete, proxy. "*" + means all.' + type: string + version: + description: Version is the API Version of the Resource. "*" + means all. + type: string + type: object + disable: + description: Disable OpenShift SSO integration and allow all + users to access the application without authentication. + This will also bypass the BasicAuth, if specified. + type: boolean + type: object type: object enableCertManager: description: Use cert-manager to secure in-cluster communication between diff --git a/config/crd/bases/operator.cryostat.io_cryostats.yaml b/config/crd/bases/operator.cryostat.io_cryostats.yaml index 6b1484a27..4bb2140fc 100644 --- a/config/crd/bases/operator.cryostat.io_cryostats.yaml +++ b/config/crd/bases/operator.cryostat.io_cryostats.yaml @@ -5146,7 +5146,11 @@ spec: properties: basicAuth: description: Reference to a secret and file name containing the - Basic authentication htpasswd file + Basic authentication htpasswd file. If deploying on OpenShift + this defines additional user accounts that can access the Cryostat + application, on top of the OpenShift user accounts which pass + the OpenShift SSO Roles checks. If not on OpenShift then this + defines the only user accounts that have access. properties: filename: description: Name of the file within the secret. @@ -5155,6 +5159,60 @@ spec: description: Name of the secret to reference. type: string type: object + openShiftSSO: + description: Configuration for OpenShift RBAC to define which + OpenShift user accounts may access the Cryostat application. + properties: + accessReview: + description: The SubjectAccessReview or TokenAccessReview + that all clients (users visiting the application via web + browser as well as CLI utilities and other programs presenting + Bearer auth tokens) must pass in order to access the application. + If not specified, the default role required is "create pods/exec" + in the Cryostat application's installation namespace. + properties: + group: + description: Group is the API Group of the Resource. "*" + means all. + type: string + name: + description: Name is the name of the resource being requested + for a "get" or deleted for a "delete". "" (empty) means + all. + type: string + namespace: + description: Namespace is the namespace of the action + being requested. Currently, there is no distinction + between no namespace and all namespaces "" (empty) is + defaulted for LocalSubjectAccessReviews "" (empty) is + empty for cluster-scoped resources "" (empty) means + "all" for namespace scoped resources from a SubjectAccessReview + or SelfSubjectAccessReview + type: string + resource: + description: Resource is one of the existing resource + types. "*" means all. + type: string + subresource: + description: Subresource is one of the existing resource + types. "" means none. + type: string + verb: + description: 'Verb is a kubernetes resource API verb, + like: get, list, watch, create, update, delete, proxy. "*" + means all.' + type: string + version: + description: Version is the API Version of the Resource. "*" + means all. + type: string + type: object + disable: + description: Disable OpenShift SSO integration and allow all + users to access the application without authentication. + This will also bypass the BasicAuth, if specified. + type: boolean + type: object type: object enableCertManager: description: Use cert-manager to secure in-cluster communication between diff --git a/config/manifests/bases/cryostat-operator.clusterserviceversion.yaml b/config/manifests/bases/cryostat-operator.clusterserviceversion.yaml index a5a31ba7b..c85c5c395 100644 --- a/config/manifests/bases/cryostat-operator.clusterserviceversion.yaml +++ b/config/manifests/bases/cryostat-operator.clusterserviceversion.yaml @@ -104,7 +104,10 @@ spec: x-descriptors: - urn:alm:descriptor:com.tectonic.ui:advanced - description: Reference to a secret and file name containing the Basic authentication - htpasswd file + htpasswd file. If deploying on OpenShift this defines additional user accounts + that can access the Cryostat application, on top of the OpenShift user accounts + which pass the OpenShift SSO Roles checks. If not on OpenShift then this + defines the only user accounts that have access. displayName: Basic Auth path: authorizationOptions.basicAuth x-descriptors: @@ -119,6 +122,24 @@ spec: path: authorizationOptions.basicAuth.secretName x-descriptors: - urn:alm:descriptor:io.kubernetes:Secret + - description: Configuration for OpenShift RBAC to define which OpenShift user + accounts may access the Cryostat application. + displayName: OpenShift SSO + path: authorizationOptions.openShiftSSO + - description: The SubjectAccessReview or TokenAccessReview that all clients + (users visiting the application via web browser as well as CLI utilities + and other programs presenting Bearer auth tokens) must pass in order to + access the application. If not specified, the default role required is "create + pods/exec" in the Cryostat application's installation namespace. + displayName: Access Review + path: authorizationOptions.openShiftSSO.accessReview + - description: Disable OpenShift SSO integration and allow all users to access + the application without authentication. This will also bypass the BasicAuth, + if specified. + displayName: Disable OpenShift SSO + path: authorizationOptions.openShiftSSO.disable + x-descriptors: + - urn:alm:descriptor:com.tectonic.ui:booleanSwitch - description: List of Flight Recorder Event Templates to preconfigure in Cryostat. displayName: Event Templates path: eventTemplates diff --git a/internal/controllers/common/resource_definitions/resource_definitions.go b/internal/controllers/common/resource_definitions/resource_definitions.go index 41f5d6e6a..a04805f85 100644 --- a/internal/controllers/common/resource_definitions/resource_definitions.go +++ b/internal/controllers/common/resource_definitions/resource_definitions.go @@ -15,6 +15,7 @@ package resource_definitions import ( + "encoding/json" "fmt" "net/url" "regexp" @@ -26,6 +27,7 @@ import ( "github.com/cryostatio/cryostat-operator/internal/controllers/constants" "github.com/cryostatio/cryostat-operator/internal/controllers/model" appsv1 "k8s.io/api/apps/v1" + authzv1 "k8s.io/api/authorization/v1" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/resource" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -79,10 +81,12 @@ const ( defaultGrafanaMemoryRequest string = "120Mi" defaultReportCpuRequest string = "200m" defaultReportMemoryRequest string = "384Mi" + OAuth2ConfigFileName string = "alpha_config.json" + OAuth2ConfigFilePath string = "/etc/oauth2_proxy/alpha_config" ) func NewDeploymentForCR(cr *model.CryostatInstance, specs *ServiceSpecs, imageTags *ImageTags, - tls *TLSConfig, fsGroup int64, openshift bool) *appsv1.Deployment { + tls *TLSConfig, fsGroup int64, openshift bool) (*appsv1.Deployment, error) { // Force one replica to avoid lock file and PVC contention replicas := int32(1) @@ -140,6 +144,10 @@ func NewDeploymentForCR(cr *model.CryostatInstance, specs *ServiceSpecs, imageTa } common.MergeLabelsAndAnnotations(&podTemplateMeta, defaultPodLabels, nil) + pod, err := NewPodForCR(cr, specs, imageTags, tls, fsGroup, openshift) + if err != nil { + return nil, err + } return &appsv1.Deployment{ ObjectMeta: deploymentMeta, Spec: appsv1.DeploymentSpec{ @@ -153,14 +161,14 @@ func NewDeploymentForCR(cr *model.CryostatInstance, specs *ServiceSpecs, imageTa }, Template: corev1.PodTemplateSpec{ ObjectMeta: podTemplateMeta, - Spec: *NewPodForCR(cr, specs, imageTags, tls, fsGroup, openshift), + Spec: *pod, }, Replicas: &replicas, Strategy: appsv1.DeploymentStrategy{ Type: appsv1.RecreateDeploymentStrategyType, }, }, - } + }, nil } func NewDeploymentForReports(cr *model.CryostatInstance, imageTags *ImageTags, tls *TLSConfig, @@ -245,14 +253,18 @@ func NewDeploymentForReports(cr *model.CryostatInstance, imageTags *ImageTags, t } func NewPodForCR(cr *model.CryostatInstance, specs *ServiceSpecs, imageTags *ImageTags, - tls *TLSConfig, fsGroup int64, openshift bool) *corev1.PodSpec { + tls *TLSConfig, fsGroup int64, openshift bool) (*corev1.PodSpec, error) { + authProxy, err := NewAuthProxyContainer(cr, specs, imageTags.OAuth2ProxyImageTag, imageTags.OpenShiftOAuthProxyImageTag, tls, openshift) + if err != nil { + return nil, err + } containers := []corev1.Container{ NewCoreContainer(cr, specs, imageTags.CoreImageTag, tls, openshift), NewGrafanaContainer(cr, imageTags.GrafanaImageTag, tls), NewJfrDatasourceContainer(cr, imageTags.DatasourceImageTag), NewStorageContainer(cr, imageTags.StorageImageTag, tls), newDatabaseContainer(cr, imageTags.DatabaseImageTag, tls), - NewAuthProxyContainer(cr, specs, imageTags.OAuth2ProxyImageTag, imageTags.OpenShiftOAuthProxyImageTag, tls, openshift), + *authProxy, } volumes := newVolumeForCR(cr) @@ -339,10 +351,31 @@ func NewPodForCR(cr *model.CryostatInstance, specs *ServiceSpecs, imageTags *Ima } volumes = append(volumes, certVolume) + if !openshift { + // if not deploying openshift-oauth-proxy then we must be deploying oauth2_proxy instead + volumes = append(volumes, corev1.Volume{ + Name: cr.Name + "-oauth2-proxy-cfg", + VolumeSource: corev1.VolumeSource{ + ConfigMap: &corev1.ConfigMapVolumeSource{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: cr.Name + "-oauth2-proxy-cfg", + }, + Items: []corev1.KeyToPath{ + { + Key: OAuth2ConfigFileName, + Path: OAuth2ConfigFileName, + Mode: &readOnlyMode, + }, + }, + }, + }, + }) + } + if isBasicAuthEnabled(cr) { volumes = append(volumes, corev1.Volume{ - Name: "auth-proxy-htpasswd", + Name: cr.Name + "-auth-proxy-htpasswd", VolumeSource: corev1.VolumeSource{ Secret: &corev1.SecretVolumeSource{ SecretName: *cr.Spec.AuthorizationOptions.BasicAuth.SecretName, @@ -425,7 +458,7 @@ func NewPodForCR(cr *model.CryostatInstance, specs *ServiceSpecs, imageTags *Ima NodeSelector: nodeSelector, Affinity: affinity, Tolerations: tolerations, - } + }, nil } func NewReportContainerResource(cr *model.CryostatInstance) *corev1.ResourceRequirements { @@ -601,15 +634,15 @@ func NewAuthProxyContainerResource(cr *model.CryostatInstance) *corev1.ResourceR } func NewAuthProxyContainer(cr *model.CryostatInstance, specs *ServiceSpecs, oauth2ProxyImageTag string, openshiftAuthProxyImageTag string, - tls *TLSConfig, openshift bool) corev1.Container { - // if (openshift) { - return NewOpenShiftAuthProxyContainer(cr, specs, openshiftAuthProxyImageTag, tls) - // } - // return NewOAuth2ProxyContainer(cr, specs, oauth2ProxyImageTag, tls) + tls *TLSConfig, openshift bool) (*corev1.Container, error) { + if openshift { + return NewOpenShiftAuthProxyContainer(cr, specs, openshiftAuthProxyImageTag, tls) + } + return NewOAuth2ProxyContainer(cr, specs, oauth2ProxyImageTag, tls) } func NewOpenShiftAuthProxyContainer(cr *model.CryostatInstance, specs *ServiceSpecs, imageTag string, - tls *TLSConfig) corev1.Container { + tls *TLSConfig) (*corev1.Container, error) { var containerSc *corev1.SecurityContext if cr.Spec.SecurityOptions != nil && cr.Spec.SecurityOptions.AuthProxySecurityContext != nil { containerSc = cr.Spec.SecurityOptions.AuthProxySecurityContext @@ -626,7 +659,7 @@ func NewOpenShiftAuthProxyContainer(cr *model.CryostatInstance, specs *ServiceSp probeHandler := corev1.ProbeHandler{ HTTPGet: &corev1.HTTPGetAction{ Port: intstr.IntOrString{IntVal: constants.AuthProxyHttpContainerPort}, - Path: "/ping", + Path: "/oauth2/healthz", Scheme: corev1.URISchemeHTTP, }, } @@ -635,27 +668,36 @@ func NewOpenShiftAuthProxyContainer(cr *model.CryostatInstance, specs *ServiceSp fmt.Sprintf("--upstream=http://localhost:%d/", constants.CryostatHTTPContainerPort), fmt.Sprintf("--upstream=http://localhost:%d/grafana/", constants.GrafanaContainerPort), fmt.Sprintf("--upstream=http://localhost:%d/storage/", constants.StoragePort), - "--cookie-secret=REPLACEME", fmt.Sprintf("--openshift-service-account=%s", cr.Name), "--proxy-websockets=true", fmt.Sprintf("--http-address=0.0.0.0:%d", constants.AuthProxyHttpContainerPort), - fmt.Sprintf( - `--openshift-sar=[{"group":"","name":"","namespace":"%s","resource":"pods","subresource":"exec","verb":"create","version":""}]`, - cr.InstallNamespace, - ), - fmt.Sprintf( - `--openshift-delegate-urls={"/":{"group":"","name":"","namespace":"%s","resource":"pods","subresource":"exec","verb":"create","version":""}}`, - cr.InstallNamespace, - ), - "--bypass-auth-for=^/health", "--proxy-prefix=/oauth2", } + if isOpenShiftAuthProxyDisabled(cr) { + args = append(args, "--bypass-auth-for=.*") + } else { + args = append(args, "--bypass-auth-for=^/health(/liveness)?$") + } + + subjectAccessReviewJson, err := json.Marshal([]authzv1.ResourceAttributes{getOpenShiftAccessReview(cr)}) + if err != nil { + return nil, err + } + args = append(args, fmt.Sprintf("--openshift-sar=%s", string(subjectAccessReviewJson))) + + delegateUrls := make(map[string]authzv1.ResourceAttributes) + delegateUrls["/"] = getOpenShiftAccessReview(cr) + tokenReviewJson, err := json.Marshal(delegateUrls) + if err != nil { + return nil, err + } + args = append(args, fmt.Sprintf("--openshift-delegate-urls=%s", string(tokenReviewJson))) volumeMounts := []corev1.VolumeMount{} if isBasicAuthEnabled(cr) { - mountPath := fmt.Sprintf("/var/run/secrets/operator.cryostat.io/%s", *cr.Spec.AuthorizationOptions.BasicAuth.SecretName) + mountPath := "/var/run/secrets/operator.cryostat.io" volumeMounts = append(volumeMounts, corev1.VolumeMount{ - Name: "auth-proxy-htpasswd", + Name: cr.Name + "-auth-proxy-htpasswd", MountPath: mountPath, ReadOnly: true, }) @@ -674,7 +716,19 @@ func NewOpenShiftAuthProxyContainer(cr *model.CryostatInstance, specs *ServiceSp args = append(args, "--https-address=") // } - return corev1.Container{ + cookieOptional := false + envsFrom := []corev1.EnvFromSource{ + { + SecretRef: &corev1.SecretEnvSource{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: cr.Name + "-oauth2-cookie", + }, + Optional: &cookieOptional, + }, + }, + } + + return &corev1.Container{ Name: cr.Name + "-auth-proxy", Image: imageTag, ImagePullPolicy: getPullPolicy(imageTag), @@ -685,20 +739,148 @@ func NewOpenShiftAuthProxyContainer(cr *model.CryostatInstance, specs *ServiceSp }, }, // Env: envs, - // EnvFrom: envsFrom, + EnvFrom: envsFrom, Resources: *NewAuthProxyContainerResource(cr), LivenessProbe: &corev1.Probe{ ProbeHandler: probeHandler, }, SecurityContext: containerSc, Args: args, + }, nil +} + +func isOpenShiftAuthProxyDisabled(cr *model.CryostatInstance) bool { + if cr.Spec.AuthorizationOptions != nil && cr.Spec.AuthorizationOptions.OpenShiftSSO != nil && cr.Spec.AuthorizationOptions.OpenShiftSSO.Disable != nil { + return *cr.Spec.AuthorizationOptions.OpenShiftSSO.Disable + } + return false +} + +func getOpenShiftAccessReview(cr *model.CryostatInstance) authzv1.ResourceAttributes { + if cr.Spec.AuthorizationOptions != nil && cr.Spec.AuthorizationOptions.OpenShiftSSO != nil && cr.Spec.AuthorizationOptions.OpenShiftSSO.AccessReview != nil { + return *cr.Spec.AuthorizationOptions.OpenShiftSSO.AccessReview + } + return getDefaultOpenShiftAccessRole(cr) +} + +func getDefaultOpenShiftAccessRole(cr *model.CryostatInstance) authzv1.ResourceAttributes { + return authzv1.ResourceAttributes{ + Namespace: cr.InstallNamespace, + Verb: "create", + Group: "", + Version: "", + Resource: "pods", + Subresource: "exec", + Name: "", } } -// func NewOAuth2ProxyContainer(cr *model.CryostatInstance, specs *ServiceSpecs, imageTag string, -// tls *TLSConfig) corev1.Container { +func NewOAuth2ProxyContainer(cr *model.CryostatInstance, specs *ServiceSpecs, imageTag string, + tls *TLSConfig) (*corev1.Container, error) { + var containerSc *corev1.SecurityContext + if cr.Spec.SecurityOptions != nil && cr.Spec.SecurityOptions.AuthProxySecurityContext != nil { + containerSc = cr.Spec.SecurityOptions.AuthProxySecurityContext + } else { + privEscalation := false + containerSc = &corev1.SecurityContext{ + AllowPrivilegeEscalation: &privEscalation, + Capabilities: &corev1.Capabilities{ + Drop: []corev1.Capability{constants.CapabilityAll}, + }, + } + } + + probeHandler := corev1.ProbeHandler{ + HTTPGet: &corev1.HTTPGetAction{ + Port: intstr.IntOrString{IntVal: constants.AuthProxyHttpContainerPort}, + Path: "/ping", + Scheme: corev1.URISchemeHTTP, + }, + } + + args := []string{ + fmt.Sprintf("--alpha-config=%s/%s", OAuth2ConfigFilePath, OAuth2ConfigFileName), + } + + envs := []corev1.EnvVar{ + { + Name: "OAUTH2_PROXY_REDIRECT_URL", + Value: fmt.Sprintf("http://localhost:%d/oauth2/callback", constants.AuthProxyHttpContainerPort), + }, + { + Name: "OAUTH2_PROXY_EMAIL_DOMAINS", + Value: "*", + }, + } + + cookieOptional := false + envsFrom := []corev1.EnvFromSource{ + { + SecretRef: &corev1.SecretEnvSource{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: cr.Name + "-oauth2-cookie", + }, + Optional: &cookieOptional, + }, + }, + } -// } + volumeMounts := []corev1.VolumeMount{ + { + Name: cr.Name + "-oauth2-proxy-cfg", + MountPath: OAuth2ConfigFilePath, + ReadOnly: true, + }, + } + + if isBasicAuthEnabled(cr) { + mountPath := "/var/run/secrets/operator.cryostat.io" + volumeMounts = append(volumeMounts, corev1.VolumeMount{ + Name: cr.Name + "-auth-proxy-htpasswd", + MountPath: mountPath, + ReadOnly: true, + }) + envs = append(envs, []corev1.EnvVar{ + { + Name: "OAUTH2_PROXY_HTPASSWD_FILE", + Value: mountPath + "/" + *cr.Spec.AuthorizationOptions.BasicAuth.Filename, + }, + { + Name: "OAUTH2_PROXY_HTPASSWD_USER_GROUP", + Value: "write", + }, + { + Name: "OAUTH2_PROXY_SKIP_AUTH_ROUTES", + Value: "^/health(/liveness)?$", + }, + }...) + } else { + envs = append(envs, corev1.EnvVar{ + Name: "OAUTH2_PROXY_SKIP_AUTH_ROUTES", + Value: ".*", + }) + } + + return &corev1.Container{ + Name: cr.Name + "-auth-proxy", + Image: imageTag, + ImagePullPolicy: getPullPolicy(imageTag), + VolumeMounts: volumeMounts, + Ports: []corev1.ContainerPort{ + { + ContainerPort: constants.AuthProxyHttpContainerPort, + }, + }, + Env: envs, + EnvFrom: envsFrom, + Resources: *NewAuthProxyContainerResource(cr), + LivenessProbe: &corev1.Probe{ + ProbeHandler: probeHandler, + }, + SecurityContext: containerSc, + Args: args, + }, nil +} func NewCoreContainerResource(cr *model.CryostatInstance) *corev1.ResourceRequirements { resources := &corev1.ResourceRequirements{} diff --git a/internal/controllers/configmaps.go b/internal/controllers/configmaps.go index 73e6c7003..0f3380fba 100644 --- a/internal/controllers/configmaps.go +++ b/internal/controllers/configmaps.go @@ -16,10 +16,14 @@ package controllers import ( "context" + "encoding/json" "fmt" + resources "github.com/cryostatio/cryostat-operator/internal/controllers/common/resource_definitions" + "github.com/cryostatio/cryostat-operator/internal/controllers/constants" "github.com/cryostatio/cryostat-operator/internal/controllers/model" corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" ) @@ -34,6 +38,87 @@ func (r *Reconciler) reconcileLockConfigMap(ctx context.Context, cr *model.Cryos return r.createOrUpdateConfigMap(ctx, cm, cr.Object) } +type oauth2ProxyAlphaConfig struct { + Server alphaConfigServer `json:"server,omitempty"` + UpstreamConfig alphaConfigUpstreamConfig `json:"upstreamConfig,omitempty"` + Providers []alphaConfigProvider `json:"providers,omitempty"` +} + +type alphaConfigServer struct { + BindAddress string `json:"BindAddress,omitempty"` +} + +type alphaConfigUpstreamConfig struct { + ProxyRawPath bool `json:"proxyRawPath,omitempty"` + Upstreams []alphaConfigUpstream `json:"upstreams,omitempty"` +} + +type alphaConfigProvider struct { + Id string `json:"id,omitempty"` + Name string `json:"name,omitempty"` + ClientId string `json:"clientId,omitempty"` + ClientSecret string `json:"clientSecret,omitempty"` + Provider string `json:"provider,omitempty"` +} + +type alphaConfigUpstream struct { + Id string `json:"id,omitempty"` + Path string `json:"path,omitempty"` + RewriteTarget string `json:"rewriteTarget,omitempty"` + Uri string `json:"uri,omitempty"` + PassHostHeader bool `json:"passHostHeader,omitempty"` + ProxyWebSockets bool `json:"proxyWebSockets,omitempty"` +} + +func (r *Reconciler) reconcileOAuth2ProxyConfig(ctx context.Context, cr *model.CryostatInstance) error { + immutable := true + cfg := &oauth2ProxyAlphaConfig{ + Server: alphaConfigServer{BindAddress: fmt.Sprintf("http://0.0.0.0:%d", constants.AuthProxyHttpContainerPort)}, + UpstreamConfig: alphaConfigUpstreamConfig{ProxyRawPath: true, Upstreams: []alphaConfigUpstream{ + { + Id: "cryostat", + Path: "/", + Uri: fmt.Sprintf("http://localhost:%d", constants.CryostatHTTPContainerPort), + }, + { + Id: "grafana", + Path: "/grafana/", + Uri: fmt.Sprintf("http://localhost:%d", constants.GrafanaContainerPort), + }, + { + Id: "storage", + Path: "^/storage/(.*)$", + RewriteTarget: "/$1", + Uri: fmt.Sprintf("http://localhost:%d", constants.StoragePort), + PassHostHeader: false, + ProxyWebSockets: false, + }, + }}, + Providers: []alphaConfigProvider{{Id: "dummy", Name: "Unused - Sign In Below", ClientId: "CLIENT_ID", ClientSecret: "CLIENT_SECRET", Provider: "google"}}, + } + + data := make(map[string]string) + json, err := json.Marshal(cfg) + if err != nil { + return err + } + data[resources.OAuth2ConfigFileName] = string(json) + cm := &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: cr.Name + "-oauth2-proxy-cfg", + Namespace: cr.InstallNamespace, + }, + Immutable: &immutable, + Data: data, + } + + if r.IsOpenShift { + return r.deleteConfigMap(ctx, cm) + } else { + return r.createOrUpdateConfigMap(ctx, cm, cr.Object) + } +} + func (r *Reconciler) createOrUpdateConfigMap(ctx context.Context, cm *corev1.ConfigMap, owner metav1.Object) error { op, err := controllerutil.CreateOrUpdate(ctx, r.Client, cm, func() error { // Set the Cryostat CR as controller @@ -48,3 +133,13 @@ func (r *Reconciler) createOrUpdateConfigMap(ctx context.Context, cm *corev1.Con r.Log.Info(fmt.Sprintf("Config Map %s", op), "name", cm.Name, "namespace", cm.Namespace) return nil } + +func (r *Reconciler) deleteConfigMap(ctx context.Context, cm *corev1.ConfigMap) error { + err := r.Client.Delete(ctx, cm) + if err != nil && !errors.IsNotFound(err) { + r.Log.Error(err, "Could not delete ConfigMap", "name", cm.Name, "namespace", cm.Namespace) + return err + } + r.Log.Info("ConfigMap deleted", "name", cm.Name, "namespace", cm.Namespace) + return nil +} diff --git a/internal/controllers/reconciler.go b/internal/controllers/reconciler.go index 7049e0421..8ed3a3b25 100644 --- a/internal/controllers/reconciler.go +++ b/internal/controllers/reconciler.go @@ -194,6 +194,11 @@ func (r *Reconciler) reconcileCryostat(ctx context.Context, cr *model.CryostatIn return reconcile.Result{}, err } + err = r.reconcileOAuth2ProxyConfig(ctx, cr) + if err != nil { + return reconcile.Result{}, err + } + err = r.reconcilePVC(ctx, cr) if err != nil { return reconcile.Result{}, err @@ -268,7 +273,10 @@ func (r *Reconciler) reconcileCryostat(ctx context.Context, cr *model.CryostatIn return reportsResult, err } - deployment := resources.NewDeploymentForCR(cr, serviceSpecs, imageTags, tlsConfig, *fsGroup, r.IsOpenShift) + deployment, err := resources.NewDeploymentForCR(cr, serviceSpecs, imageTags, tlsConfig, *fsGroup, r.IsOpenShift) + if err != nil { + return reconcile.Result{}, err + } err = r.createOrUpdateDeployment(ctx, deployment, cr.Object) if err != nil { return reconcile.Result{}, err diff --git a/internal/controllers/reconciler_test.go b/internal/controllers/reconciler_test.go index 3bfd96fa2..7d8ed2139 100644 --- a/internal/controllers/reconciler_test.go +++ b/internal/controllers/reconciler_test.go @@ -66,7 +66,7 @@ type cryostatTestInput struct { func (c *controllerTest) commonBeforeEach() *cryostatTestInput { t := &cryostatTestInput{ TestReconcilerConfig: test.TestReconcilerConfig{ - GeneratedPasswords: []string{"grafana", "credentials_database", "encryption_key", "object_storage", "jmx", "keystore"}, + GeneratedPasswords: []string{"auth_cookie_secret", "grafana", "credentials_database", "encryption_key", "object_storage", "jmx", "keystore"}, }, TestResources: &test.TestResources{ Name: "cryostat", diff --git a/internal/controllers/secrets.go b/internal/controllers/secrets.go index b2dc3209c..6118ce55c 100644 --- a/internal/controllers/secrets.go +++ b/internal/controllers/secrets.go @@ -26,6 +26,9 @@ import ( ) func (r *Reconciler) reconcileSecrets(ctx context.Context, cr *model.CryostatInstance) error { + if err := r.reconcileAuthProxyCookieSecret(ctx, cr); err != nil { + return err + } if err := r.reconcileGrafanaSecret(ctx, cr); err != nil { return err } @@ -38,6 +41,27 @@ func (r *Reconciler) reconcileSecrets(ctx context.Context, cr *model.CryostatIns return r.reconcileJMXSecret(ctx, cr) } +func (r *Reconciler) reconcileAuthProxyCookieSecret(ctx context.Context, cr *model.CryostatInstance) error { + secret := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: cr.Name + "-oauth2-cookie", + Namespace: cr.InstallNamespace, + }, + } + + return r.createOrUpdateSecret(ctx, secret, cr.Object, func() error { + if secret.StringData == nil { + secret.StringData = map[string]string{} + } + + // secret is generated, so don't regenerate it when updating + if secret.CreationTimestamp.IsZero() { + secret.StringData["OAUTH2_PROXY_COOKIE_SECRET"] = r.GenPasswd(32) + } + return nil + }) +} + func (r *Reconciler) reconcileGrafanaSecret(ctx context.Context, cr *model.CryostatInstance) error { secret := &corev1.Secret{ ObjectMeta: metav1.ObjectMeta{ diff --git a/internal/test/resources.go b/internal/test/resources.go index 0e046e795..fe667e290 100644 --- a/internal/test/resources.go +++ b/internal/test/resources.go @@ -2111,6 +2111,27 @@ func (r *TestResources) newVolumes(certProjections []corev1.VolumeProjection) [] }, }) + if !r.OpenShift { + readOnlyMode := int32(0440) + volumes = append(volumes, corev1.Volume{ + Name: r.Name + "-oauth2-proxy-cfg", + VolumeSource: corev1.VolumeSource{ + ConfigMap: &corev1.ConfigMapVolumeSource{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: r.Name + "-oauth2-proxy-cfg", + }, + Items: []corev1.KeyToPath{ + { + Key: "alpha_config.json", + Path: "alpha_config.json", + Mode: &readOnlyMode, + }, + }, + }, + }, + }) + } + return volumes }