This is an automated email from the ASF dual-hosted git repository.

thelabdude pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/solr-operator.git


The following commit(s) were added to refs/heads/main by this push:
     new 505e244  Support to bootstrap from a user-supplied security.json and 
minor refactorings to pass auth headers through a Context (#356)
505e244 is described below

commit 505e244c5ae74a2f49565623028e7d5195d7eed7
Author: Timothy Potter <[email protected]>
AuthorDate: Fri Oct 29 12:42:26 2021 -0600

    Support to bootstrap from a user-supplied security.json and minor 
refactorings to pass auth headers through a Context (#356)
---
 api/v1beta1/solrcloud_types.go                     |   6 +
 api/v1beta1/zz_generated.deepcopy.go               |   7 +-
 config/crd/bases/solr.apache.org_solrclouds.yaml   |  15 ++
 controllers/solrbackup_controller.go               |  18 +-
 controllers/solrcloud_controller.go                |   9 +-
 .../solrcloud_controller_basic_auth_test.go        | 103 +++++++--
 controllers/util/backup_util.go                    |  13 +-
 controllers/util/solr_api/api.go                   |  11 +-
 controllers/util/solr_security_util.go             | 253 +++++++++++++--------
 controllers/util/solr_update_util.go               |   7 +-
 controllers/util/solr_util.go                      |   6 +-
 docs/solr-cloud/solr-cloud-crd.md                  |  53 +++--
 helm/solr-operator/Chart.yaml                      |   7 +
 helm/solr-operator/crds/crds.yaml                  |  15 ++
 helm/solr/README.md                                |   2 +
 helm/solr/values.yaml                              |   3 +
 16 files changed, 376 insertions(+), 152 deletions(-)

diff --git a/api/v1beta1/solrcloud_types.go b/api/v1beta1/solrcloud_types.go
index fb83951..e61aa76 100644
--- a/api/v1beta1/solrcloud_types.go
+++ b/api/v1beta1/solrcloud_types.go
@@ -1459,4 +1459,10 @@ type SolrSecurityOptions struct {
        // endpoints with credentials sourced from an env var instead of HTTP 
directly.
        // +optional
        ProbesRequireAuth bool `json:"probesRequireAuth,omitempty"`
+
+       // Configure a user-provided security.json from a secret to allow for 
advanced security config.
+       // If not specified, the operator bootstraps a security.json with basic 
auth enabled.
+       // This is a bootstrapping config only; once Solr is initialized, the 
security config should be managed by the security API.
+       // +optional
+       BootstrapSecurityJson *corev1.SecretKeySelector 
`json:"bootstrapSecurityJson,omitempty"`
 }
diff --git a/api/v1beta1/zz_generated.deepcopy.go 
b/api/v1beta1/zz_generated.deepcopy.go
index 01ef438..a581e09 100644
--- a/api/v1beta1/zz_generated.deepcopy.go
+++ b/api/v1beta1/zz_generated.deepcopy.go
@@ -970,7 +970,7 @@ func (in *SolrCloudSpec) DeepCopyInto(out *SolrCloudSpec) {
        if in.SolrSecurity != nil {
                in, out := &in.SolrSecurity, &out.SolrSecurity
                *out = new(SolrSecurityOptions)
-               **out = **in
+               (*in).DeepCopyInto(*out)
        }
        if in.BackupRepositories != nil {
                in, out := &in.BackupRepositories, &out.BackupRepositories
@@ -1247,6 +1247,11 @@ func (in *SolrReference) DeepCopy() *SolrReference {
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, 
writing into out. in must be non-nil.
 func (in *SolrSecurityOptions) DeepCopyInto(out *SolrSecurityOptions) {
        *out = *in
+       if in.BootstrapSecurityJson != nil {
+               in, out := &in.BootstrapSecurityJson, &out.BootstrapSecurityJson
+               *out = new(v1.SecretKeySelector)
+               (*in).DeepCopyInto(*out)
+       }
 }
 
 // DeepCopy is an autogenerated deepcopy function, copying the receiver, 
creating a new SolrSecurityOptions.
diff --git a/config/crd/bases/solr.apache.org_solrclouds.yaml 
b/config/crd/bases/solr.apache.org_solrclouds.yaml
index dc363fd..7fff8bd 100644
--- a/config/crd/bases/solr.apache.org_solrclouds.yaml
+++ b/config/crd/bases/solr.apache.org_solrclouds.yaml
@@ -5895,6 +5895,21 @@ spec:
                   basicAuthSecret:
                     description: "Secret (kubernetes.io/basic-auth) containing 
credentials the operator should use for API requests to secure Solr pods. If 
you provide this secret, then the operator assumes you've also configured your 
own security.json file and uploaded it to Solr. If you change the password for 
this user using the Solr security API, then you *must* update the secret with 
the new password or the operator will be  locked out of Solr and API requests 
will fail, ultimately  [...]
                     type: string
+                  bootstrapSecurityJson:
+                    description: Configure a user-provided security.json from 
a secret to allow for advanced security config. If not specified, the operator 
bootstraps a security.json with basic auth enabled. This is a bootstrapping 
config only; once Solr is initialized, the security config should be managed by 
the security API.
+                    properties:
+                      key:
+                        description: The key of the secret to select from.  
Must be a valid secret key.
+                        type: string
+                      name:
+                        description: 'Name of the referent. More info: 
https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names 
TODO: Add other useful fields. apiVersion, kind, uid?'
+                        type: string
+                      optional:
+                        description: Specify whether the Secret or its key 
must be defined
+                        type: boolean
+                    required:
+                    - key
+                    type: object
                   probesRequireAuth:
                     description: Flag to indicate if the configured HTTP 
endpoint(s) used for the probes require authentication; defaults to false. If 
you set to true, then probes will use a local command on the main container to 
hit the secured endpoints with credentials sourced from an env var instead of 
HTTP directly.
                     type: boolean
diff --git a/controllers/solrbackup_controller.go 
b/controllers/solrbackup_controller.go
index 228036b..77373e3 100644
--- a/controllers/solrbackup_controller.go
+++ b/controllers/solrbackup_controller.go
@@ -26,7 +26,6 @@ import (
        "github.com/apache/solr-operator/controllers/util"
        "github.com/go-logr/logr"
        batchv1 "k8s.io/api/batch/v1"
-       corev1 "k8s.io/api/core/v1"
        "k8s.io/apimachinery/pkg/api/errors"
        metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
        "k8s.io/apimachinery/pkg/runtime"
@@ -160,13 +159,12 @@ func (r *SolrBackupReconciler) 
reconcileSolrCloudBackup(ctx context.Context, bac
                return nil, collectionBackupsFinished, actionTaken, err
        }
 
-       var httpHeaders map[string]string
+       // Add any additional values needed to Authn to Solr to the Context 
used when invoking the API
        if solrCloud.Spec.SolrSecurity != nil {
-               basicAuthSecret := &corev1.Secret{}
-               if err := r.Get(ctx, types.NamespacedName{Name: 
solrCloud.BasicAuthSecretName(), Namespace: solrCloud.Namespace}, 
basicAuthSecret); err != nil {
+               ctx, err = util.AddAuthToContext(ctx, &r.Client, 
solrCloud.Spec.SolrSecurity, solrCloud.Namespace)
+               if err != nil {
                        return nil, collectionBackupsFinished, actionTaken, err
                }
-               httpHeaders = map[string]string{"Authorization": 
util.BasicAuthHeader(basicAuthSecret)}
        }
 
        // First check if the collection backups have been completed
@@ -208,7 +206,7 @@ func (r *SolrBackupReconciler) reconcileSolrCloudBackup(ctx 
context.Context, bac
 
        // Go through each collection specified and reconcile the backup.
        for _, collection := range backup.Spec.Collections {
-               _, err = reconcileSolrCollectionBackup(backup, solrCloud, 
backupRepository, collection, httpHeaders, logger)
+               _, err = reconcileSolrCollectionBackup(ctx, backup, solrCloud, 
backupRepository, collection, logger)
        }
 
        // First check if the collection backups have been completed
@@ -217,7 +215,7 @@ func (r *SolrBackupReconciler) reconcileSolrCloudBackup(ctx 
context.Context, bac
        return solrCloud, collectionBackupsFinished, actionTaken, err
 }
 
-func reconcileSolrCollectionBackup(backup *solrv1beta1.SolrBackup, solrCloud 
*solrv1beta1.SolrCloud, backupRepository *solrv1beta1.SolrBackupRepository, 
collection string, httpHeaders map[string]string, logger logr.Logger) (finished 
bool, err error) {
+func reconcileSolrCollectionBackup(ctx context.Context, backup 
*solrv1beta1.SolrBackup, solrCloud *solrv1beta1.SolrCloud, backupRepository 
*solrv1beta1.SolrBackupRepository, collection string, logger logr.Logger) 
(finished bool, err error) {
        now := metav1.Now()
        collectionBackupStatus := solrv1beta1.CollectionBackupStatus{}
        collectionBackupStatus.Collection = collection
@@ -233,7 +231,7 @@ func reconcileSolrCollectionBackup(backup 
*solrv1beta1.SolrBackup, solrCloud *so
        // If the collection backup hasn't started, start it
        if !collectionBackupStatus.InProgress && 
!collectionBackupStatus.Finished {
                // Start the backup by calling solr
-               started, err := util.StartBackupForCollection(solrCloud, 
backupRepository, backup, collection, httpHeaders, logger)
+               started, err := util.StartBackupForCollection(ctx, solrCloud, 
backupRepository, backup, collection, logger)
                if err != nil {
                        return true, err
                }
@@ -244,7 +242,7 @@ func reconcileSolrCollectionBackup(backup 
*solrv1beta1.SolrBackup, solrCloud *so
                collectionBackupStatus.BackupName = 
util.FullCollectionBackupName(collection, backup.Name)
        } else if collectionBackupStatus.InProgress {
                // Check the state of the backup, when it is in progress, and 
update the state accordingly
-               finished, successful, asyncStatus, err := 
util.CheckBackupForCollection(solrCloud, collection, backup.Name, httpHeaders, 
logger)
+               finished, successful, asyncStatus, err := 
util.CheckBackupForCollection(ctx, solrCloud, collection, backup.Name, logger)
                if err != nil {
                        return false, err
                }
@@ -259,7 +257,7 @@ func reconcileSolrCollectionBackup(backup 
*solrv1beta1.SolrBackup, solrCloud *so
                                collectionBackupStatus.FinishTime = &now
                        }
 
-                       err = util.DeleteAsyncInfoForBackup(solrCloud, 
collection, backup.Name, httpHeaders, logger)
+                       err = util.DeleteAsyncInfoForBackup(ctx, solrCloud, 
collection, backup.Name, logger)
                } else {
                        collectionBackupStatus.AsyncBackupStatus = asyncStatus
                }
diff --git a/controllers/solrcloud_controller.go 
b/controllers/solrcloud_controller.go
index a5875ae..b854855 100644
--- a/controllers/solrcloud_controller.go
+++ b/controllers/solrcloud_controller.go
@@ -408,14 +408,17 @@ func (r *SolrCloudReconciler) Reconcile(ctx 
context.Context, req ctrl.Request) (
                }
 
                // If authn enabled on Solr, we need to pass the auth header
-               var authHeader map[string]string
                if security != nil {
-                       authHeader = security.AuthHeader()
+                       ctx, err = security.AddAuthToContext(ctx)
+                       if err != nil {
+                               updateLogger.Error(err, "failed to create 
Authorization header when reconciling", "SolrCloud", instance.Name)
+                               return requeueOrNot, err
+                       }
                }
 
                // Pick which pods should be deleted for an update.
                // Don't exit on an error, which would only occur because of an 
HTTP Exception. Requeue later instead.
-               additionalPodsToUpdate, retryLater := 
util.DeterminePodsSafeToUpdate(instance, outOfDatePods, 
int(newStatus.ReadyReplicas), availableUpdatedPodCount, 
len(outOfDatePodsNotStarted), updateLogger, authHeader)
+               additionalPodsToUpdate, retryLater := 
util.DeterminePodsSafeToUpdate(ctx, instance, outOfDatePods, 
int(newStatus.ReadyReplicas), availableUpdatedPodCount, 
len(outOfDatePodsNotStarted), updateLogger)
                podsToUpdate = append(podsToUpdate, additionalPodsToUpdate...)
 
                for _, pod := range podsToUpdate {
diff --git a/controllers/solrcloud_controller_basic_auth_test.go 
b/controllers/solrcloud_controller_basic_auth_test.go
index 433f67e..05594a5 100644
--- a/controllers/solrcloud_controller_basic_auth_test.go
+++ b/controllers/solrcloud_controller_basic_auth_test.go
@@ -27,6 +27,7 @@ import (
        appsv1 "k8s.io/api/apps/v1"
        corev1 "k8s.io/api/core/v1"
        metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+       "k8s.io/apimachinery/pkg/util/intstr"
 )
 
 var _ = FDescribe("SolrCloud controller - Basic Auth", func() {
@@ -82,6 +83,34 @@ var _ = FDescribe("SolrCloud controller - Basic Auth", 
func() {
                })
        })
 
+       FContext("Boostrap Security JSON with Custom Probe Paths", func() {
+               BeforeEach(func() {
+                       customHandler := corev1.Handler{
+                               HTTPGet: &corev1.HTTPGetAction{
+                                       Scheme: corev1.URISchemeHTTP,
+                                       Path:   "/solr/readyz",
+                                       Port:   intstr.FromInt(8983),
+                               },
+                       }
+
+                       // verify users can vary the probe path and the secure 
probe exec command uses them
+                       solrCloud.Spec.CustomSolrKubeOptions = 
solrv1beta1.CustomSolrKubeOptions{
+                               PodOptions: &solrv1beta1.PodOptions{
+                                       LivenessProbe:  &corev1.Probe{Handler: 
customHandler},
+                                       ReadinessProbe: &corev1.Probe{Handler: 
customHandler},
+                               },
+                       }
+
+                       solrCloud.Spec.SolrSecurity = 
&solrv1beta1.SolrSecurityOptions{
+                               AuthenticationType: solrv1beta1.Basic,
+                               ProbesRequireAuth:  true,
+                       }
+               })
+               FIt("has the correct resources", func() {
+                       expectStatefulSetBasicAuthConfig(ctx, solrCloud, true)
+               })
+       })
+
        FContext("Boostrap Security JSON with ZK ACLs", func() {
                BeforeEach(func() {
                        solrCloud.Spec.SolrSecurity = 
&solrv1beta1.SolrSecurityOptions{
@@ -144,6 +173,34 @@ var _ = FDescribe("SolrCloud controller - Basic Auth", 
func() {
                        expectStatefulSetBasicAuthConfig(ctx, solrCloud, false)
                })
        })
+
+       FContext("User Provided Credentials and security.json secret", func() {
+               BeforeEach(func() {
+                       basicAuthSecretName := "my-basic-auth-secret"
+                       solrCloud.Spec.SolrSecurity = 
&solrv1beta1.SolrSecurityOptions{
+                               AuthenticationType: solrv1beta1.Basic,
+                               BasicAuthSecret:    basicAuthSecretName,
+                               BootstrapSecurityJson: 
&corev1.SecretKeySelector{
+                                       LocalObjectReference: 
corev1.LocalObjectReference{Name: "my-security-json"},
+                                       Key:                  
util.SecurityJsonFile,
+                               },
+                       }
+               })
+               FIt("has the correct resources", func() {
+                       By("Making sure that no statefulSet exists until the 
BasicAuth Secret is created")
+                       expectNoStatefulSet(ctx, solrCloud, 
solrCloud.StatefulSetName())
+
+                       By("Create the basicAuth secret")
+                       basicAuthSecret := 
createBasicAuthSecret(solrCloud.Spec.SolrSecurity.BasicAuthSecret, 
solrv1beta1.DefaultBasicAuthUsername, solrCloud.Namespace)
+                       Expect(k8sClient.Create(ctx, 
basicAuthSecret)).To(Succeed(), "Could not create the necessary basicAuth 
secret")
+
+                       By("Create the security.json Secret")
+                       createMockSecurityJsonSecret(ctx, "my-security-json", 
solrCloud.Namespace)
+
+                       By("Make sure the StatefulSet is created and configured 
correctly")
+                       expectStatefulSetBasicAuthConfig(ctx, solrCloud, false)
+               })
+       })
 })
 
 var boostrapedSecretKeys = []string{
@@ -155,8 +212,13 @@ var boostrapedSecretKeys = []string{
 func expectStatefulSetBasicAuthConfig(ctx context.Context, sc 
*solrv1beta1.SolrCloud, expectBootstrapSecret bool) *appsv1.StatefulSet {
        Expect(sc.Spec.SolrSecurity).To(Not(BeNil()), "solrSecurity is not 
configured for this SolrCloud instance!")
 
+       expProbePath := "/solr/admin/info/system"
+       if sc.Spec.CustomSolrKubeOptions.PodOptions != nil && 
sc.Spec.CustomSolrKubeOptions.PodOptions.LivenessProbe != nil {
+               expProbePath = 
sc.Spec.CustomSolrKubeOptions.PodOptions.LivenessProbe.HTTPGet.Path
+       }
+
        stateful := expectStatefulSetWithChecks(ctx, sc, sc.StatefulSetName(), 
func(g Gomega, found *appsv1.StatefulSet) {
-               expectBasicAuthConfigOnPodTemplateWithGomega(g, sc, 
&found.Spec.Template, expectBootstrapSecret)
+               expectBasicAuthConfigOnPodTemplateWithGomega(g, sc, 
&found.Spec.Template, expectBootstrapSecret, expProbePath)
        })
 
        expectSecretWithChecks(ctx, sc, sc.BasicAuthSecretName(), func(innerG 
Gomega, found *corev1.Secret) {
@@ -192,12 +254,7 @@ func expectStatefulSetBasicAuthConfig(ctx context.Context, 
sc *solrv1beta1.SolrC
 }
 
 // Ensures config is setup for basic-auth enabled Solr pods
-func expectBasicAuthConfigOnPodTemplate(solrCloud *solrv1beta1.SolrCloud, 
podTemplate *corev1.PodTemplateSpec, expectBootstrapSecret bool) 
*corev1.Container {
-       return expectBasicAuthConfigOnPodTemplateWithGomega(Default, solrCloud, 
podTemplate, expectBootstrapSecret)
-}
-
-// Ensures config is setup for basic-auth enabled Solr pods
-func expectBasicAuthConfigOnPodTemplateWithGomega(g Gomega, solrCloud 
*solrv1beta1.SolrCloud, podTemplate *corev1.PodTemplateSpec, 
expectBootstrapSecret bool) *corev1.Container {
+func expectBasicAuthConfigOnPodTemplateWithGomega(g Gomega, solrCloud 
*solrv1beta1.SolrCloud, podTemplate *corev1.PodTemplateSpec, 
expectBootstrapSecret bool, expProbePath string) *corev1.Container {
        // check the env vars needed for the probes to work with auth
        g.Expect(podTemplate.Spec.Containers).To(Not(BeEmpty()), "Solr Pod 
requires containers")
        mainContainer := podTemplate.Spec.Containers[0]
@@ -233,8 +290,8 @@ func expectBasicAuthConfigOnPodTemplateWithGomega(g Gomega, 
solrCloud *solrv1bet
                        
"-Dsolr.httpclient.builder.factory=org.apache.solr.client.solrj.impl.PreemptiveBasicAuthClientBuilderFactory
 "+
                        "-Dsolr.install.dir=\"/opt/solr\" 
-Dlog4j.configurationFile=\"/opt/solr/server/resources/log4j2-console.xml\" "+
                        "-classpath 
\"/opt/solr/server/solr-webapp/webapp/WEB-INF/lib/*:/opt/solr/server/lib/ext/*:/opt/solr/server/lib/*\"
 "+
-                       "org.apache.solr.util.SolrCLI api -get 
http://localhost:8983/solr/admin/info/system";,
-                       solrCloud.Name, solrCloud.Name)
+                       "org.apache.solr.util.SolrCLI api -get 
http://localhost:8983%s";,
+                       solrCloud.Name, solrCloud.Name, expProbePath)
                g.Expect(mainContainer.LivenessProbe).To(Not(BeNil()), "main 
container should have a liveness probe defined")
                g.Expect(mainContainer.LivenessProbe.Exec).To(Not(BeNil()), 
"liveness probe should have an exec when auth is enabled")
                
g.Expect(mainContainer.LivenessProbe.Exec.Command).To(Not(BeEmpty()), "liveness 
probe command cannot be empty")
@@ -248,7 +305,7 @@ func expectBasicAuthConfigOnPodTemplateWithGomega(g Gomega, 
solrCloud *solrv1bet
        }
 
        // if no user-provided auth secret, then check that security.json gets 
bootstrapped correctly
-       if solrCloud.Spec.SolrSecurity.BasicAuthSecret == "" {
+       if solrCloud.Spec.SolrSecurity.BasicAuthSecret == "" || 
solrCloud.Spec.SolrSecurity.BootstrapSecurityJson != nil {
                // initContainers
                g.Expect(podTemplate.Spec.InitContainers).To(Not(BeEmpty()), 
"The Solr Pod template requires an init container to bootstrap the 
security.json")
                var expInitContainer *corev1.Container = nil
@@ -259,7 +316,7 @@ func expectBasicAuthConfigOnPodTemplateWithGomega(g Gomega, 
solrCloud *solrv1bet
                        }
                }
 
-               if expectBootstrapSecret {
+               if expectBootstrapSecret || 
solrCloud.Spec.SolrSecurity.BootstrapSecurityJson != nil {
                        // if the zookeeperRef has ACLs set, verify the env 
vars were set correctly for this initContainer
                        allACL, _ := solrCloud.Spec.ZookeeperRef.GetACLs()
                        if allACL != nil {
@@ -269,12 +326,7 @@ func expectBasicAuthConfigOnPodTemplateWithGomega(g 
Gomega, solrCloud *solrv1bet
                                testACLEnvVarsWithGomega(g, 
expInitContainer.Env[3:len(expInitContainer.Env)-2], true)
                        } // else this ref not using ACLs
 
-                       g.Expect(expInitContainer).To(Not(BeNil()), "Didn't 
find the setup-zk InitContainer in the sts!")
-                       expCmd := 
"ZK_SECURITY_JSON=$(/opt/solr/server/scripts/cloud-scripts/zkcli.sh -zkhost 
${ZK_HOST} -cmd get /security.json); " +
-                               "if [ ${#ZK_SECURITY_JSON} -lt 3 ]; then " +
-                               "echo $SECURITY_JSON > /tmp/security.json; " +
-                               
"/opt/solr/server/scripts/cloud-scripts/zkcli.sh -zkhost ${ZK_HOST} -cmd 
putfile /security.json /tmp/security.json; echo \"put security.json in ZK\"; fi"
-                       
g.Expect(expInitContainer.Command[2]).To(ContainSubstring(expCmd), "setup-zk 
initContainer not configured to bootstrap security.json!")
+                       expectPutSecurityJsonInZkCmd(g, expInitContainer)
                } else {
                        g.Expect(expInitContainer).To(Or(
                                BeNil(),
@@ -292,3 +344,20 @@ func expectBasicAuthConfigOnPodTemplateWithGomega(g 
Gomega, solrCloud *solrv1bet
 
        return &mainContainer // return as a convenience in case tests want to 
do more checking on the main container
 }
+
+func expectPutSecurityJsonInZkCmd(g Gomega, expInitContainer 
*corev1.Container) {
+       g.Expect(expInitContainer).To(Not(BeNil()), "Didn't find the setup-zk 
InitContainer in the sts!")
+       expCmd := 
"ZK_SECURITY_JSON=$(/opt/solr/server/scripts/cloud-scripts/zkcli.sh -zkhost 
${ZK_HOST} -cmd get /security.json); " +
+               "if [ ${#ZK_SECURITY_JSON} -lt 3 ]; then " +
+               "echo $SECURITY_JSON > /tmp/security.json; " +
+               "/opt/solr/server/scripts/cloud-scripts/zkcli.sh -zkhost 
${ZK_HOST} -cmd putfile /security.json /tmp/security.json; echo \"put 
security.json in ZK\"; fi"
+       g.Expect(expInitContainer.Command[2]).To(ContainSubstring(expCmd), 
"setup-zk initContainer not configured to bootstrap security.json!")
+}
+
+func createMockSecurityJsonSecret(ctx context.Context, name string, ns string) 
corev1.Secret {
+       secData := map[string]string{}
+       secData[util.SecurityJsonFile] = "{}"
+       sec := corev1.Secret{ObjectMeta: metav1.ObjectMeta{Name: name, 
Namespace: ns}, StringData: secData}
+       Expect(k8sClient.Create(ctx, &sec)).To(Succeed(), "Could not create 
mock security.json secret")
+       return sec
+}
diff --git a/controllers/util/backup_util.go b/controllers/util/backup_util.go
index d0dd2d8..2670ca9 100644
--- a/controllers/util/backup_util.go
+++ b/controllers/util/backup_util.go
@@ -19,6 +19,7 @@ package util
 
 import (
        "bytes"
+       "context"
        "fmt"
        solr "github.com/apache/solr-operator/api/v1beta1"
        "github.com/apache/solr-operator/controllers/util/solr_api"
@@ -320,12 +321,12 @@ func GenerateQueryParamsForBackup(backupRepository 
*solr.SolrBackupRepository, b
        return queryParams
 }
 
-func StartBackupForCollection(cloud *solr.SolrCloud, backupRepository 
*solr.SolrBackupRepository, backup *solr.SolrBackup, collection string, 
httpHeaders map[string]string, logger logr.Logger) (success bool, err error) {
+func StartBackupForCollection(ctx context.Context, cloud *solr.SolrCloud, 
backupRepository *solr.SolrBackupRepository, backup *solr.SolrBackup, 
collection string, logger logr.Logger) (success bool, err error) {
        queryParams := GenerateQueryParamsForBackup(backupRepository, backup, 
collection)
        resp := &solr_api.SolrAsyncResponse{}
 
        logger.Info("Calling to start collection backup", "solrCloud", 
cloud.Name, "collection", collection)
-       err = solr_api.CallCollectionsApi(cloud, queryParams, httpHeaders, resp)
+       err = solr_api.CallCollectionsApi(ctx, cloud, queryParams, resp)
 
        if err == nil {
                if resp.ResponseHeader.Status == 0 {
@@ -338,7 +339,7 @@ func StartBackupForCollection(cloud *solr.SolrCloud, 
backupRepository *solr.Solr
        return success, err
 }
 
-func CheckBackupForCollection(cloud *solr.SolrCloud, collection string, 
backupName string, httpHeaders map[string]string, logger logr.Logger) (finished 
bool, success bool, asyncStatus string, err error) {
+func CheckBackupForCollection(ctx context.Context, cloud *solr.SolrCloud, 
collection string, backupName string, logger logr.Logger) (finished bool, 
success bool, asyncStatus string, err error) {
        queryParams := url.Values{}
        queryParams.Add("action", "REQUESTSTATUS")
        queryParams.Add("requestid", AsyncIdForCollectionBackup(collection, 
backupName))
@@ -346,7 +347,7 @@ func CheckBackupForCollection(cloud *solr.SolrCloud, 
collection string, backupNa
        resp := &solr_api.SolrAsyncResponse{}
 
        logger.Info("Calling to check on collection backup", "solrCloud", 
cloud.Name, "collection", collection)
-       err = solr_api.CallCollectionsApi(cloud, queryParams, httpHeaders, resp)
+       err = solr_api.CallCollectionsApi(ctx, cloud, queryParams, resp)
 
        if err == nil {
                if resp.ResponseHeader.Status == 0 {
@@ -367,7 +368,7 @@ func CheckBackupForCollection(cloud *solr.SolrCloud, 
collection string, backupNa
        return finished, success, asyncStatus, err
 }
 
-func DeleteAsyncInfoForBackup(cloud *solr.SolrCloud, collection string, 
backupName string, httpHeaders map[string]string, logger logr.Logger) (err 
error) {
+func DeleteAsyncInfoForBackup(ctx context.Context, cloud *solr.SolrCloud, 
collection string, backupName string, logger logr.Logger) (err error) {
        queryParams := url.Values{}
        queryParams.Add("action", "DELETESTATUS")
        queryParams.Add("requestid", AsyncIdForCollectionBackup(collection, 
backupName))
@@ -375,7 +376,7 @@ func DeleteAsyncInfoForBackup(cloud *solr.SolrCloud, 
collection string, backupNa
        resp := &solr_api.SolrAsyncResponse{}
 
        logger.Info("Calling to delete async info for backup command.", 
"solrCloud", cloud.Name, "collection", collection)
-       err = solr_api.CallCollectionsApi(cloud, queryParams, httpHeaders, resp)
+       err = solr_api.CallCollectionsApi(ctx, cloud, queryParams, resp)
        if err != nil {
                logger.Error(err, "Error deleting async data for collection 
backup", "solrCloud", cloud.Name, "collection", collection)
        }
diff --git a/controllers/util/solr_api/api.go b/controllers/util/solr_api/api.go
index 8a6dd55..51ce412 100644
--- a/controllers/util/solr_api/api.go
+++ b/controllers/util/solr_api/api.go
@@ -18,6 +18,7 @@
 package solr_api
 
 import (
+       "context"
        "crypto/tls"
        "encoding/json"
        "fmt"
@@ -28,6 +29,10 @@ import (
        "net/url"
 )
 
+const (
+       HTTP_HEADERS_CONTEXT_KEY = "HTTP_HEADERS"
+)
+
 // Used to call a Solr pod over https when using a self-signed cert
 // It's "insecure" but is only used for internal communication, such as 
getting cluster status
 // so if you're worried about this, don't use a self-signed cert
@@ -65,7 +70,7 @@ type SolrAsyncStatus struct {
        Message string `json:"msg"`
 }
 
-func CallCollectionsApi(cloud *solr.SolrCloud, urlParams url.Values, 
httpHeaders map[string]string, response interface{}) (err error) {
+func CallCollectionsApi(ctx context.Context, cloud *solr.SolrCloud, urlParams 
url.Values, response interface{}) (err error) {
        cloudUrl := solr.InternalURLForCloud(cloud)
 
        client := noVerifyTLSHttpClient
@@ -81,8 +86,8 @@ func CallCollectionsApi(cloud *solr.SolrCloud, urlParams 
url.Values, httpHeaders
 
        req, err := http.NewRequest("GET", cloudUrl, nil)
 
-       // mainly for doing basic-auth
-       if httpHeaders != nil {
+       // Any custom HTTP headers passed through the Context
+       if httpHeaders, hasHeaders := 
ctx.Value(HTTP_HEADERS_CONTEXT_KEY).(map[string]string); hasHeaders {
                for key, header := range httpHeaders {
                        req.Header.Add(key, header)
                }
diff --git a/controllers/util/solr_security_util.go 
b/controllers/util/solr_security_util.go
index 9701ab2..c483181 100644
--- a/controllers/util/solr_security_util.go
+++ b/controllers/util/solr_security_util.go
@@ -24,6 +24,7 @@ import (
        "encoding/json"
        "fmt"
        solr "github.com/apache/solr-operator/api/v1beta1"
+       "github.com/apache/solr-operator/controllers/util/solr_api"
        appsv1 "k8s.io/api/apps/v1"
        corev1 "k8s.io/api/core/v1"
        "k8s.io/apimachinery/pkg/api/errors"
@@ -43,101 +44,134 @@ const (
        DefaultProbePath       = "/admin/info/system"
 )
 
+// Utility struct holding security related config and objects resolved at 
runtime needed during reconciliation,
+// such as the secret holding credentials the operator should use to make 
calls to secure Solr
 type SecurityConfig struct {
-       BasicAuthSecret *corev1.Secret
-       SecurityJson    string
+       SolrSecurity      *solr.SolrSecurityOptions
+       CredentialsSecret *corev1.Secret
+       SecurityJson      string
+       SecurityJsonSrc   *corev1.EnvVarSource
 }
 
 // Given a SolrCloud instance and an API service client, produce a 
SecurityConfig needed to enable Solr security
 func ReconcileSecurityConfig(ctx context.Context, client *client.Client, 
instance *solr.SolrCloud) (*SecurityConfig, error) {
-       reader := *client
+       sec := instance.Spec.SolrSecurity
+       if sec.AuthenticationType == solr.Basic {
+               return reconcileForBasicAuth(ctx, client, instance)
+       }
 
-       security := &SecurityConfig{}
-       basicAuthSecret := &corev1.Secret{}
+       // shouldn't ever get here since the YAML would be validated against 
the enum before this, but keeping it here for human readers to grok the overall 
flow
+       return nil, fmt.Errorf("%s not supported! Only 'Basic' authentication 
is supported by the Solr operator", sec.AuthenticationType)
+}
 
+// Reconcile the credentials and supporting config needed to make calls to 
Solr secured with basic auth
+// Also, bootstraps an initial security.json config if not supplied by the user
+// However, if users provide their own security.json, then they must also 
provide the basic auth secret containing
+// credentials the operator should use for making calls to Solr. In other 
words, we don't try to infuse a new user into
+// the user-provided security.json as that could get messy.
+func reconcileForBasicAuth(ctx context.Context, client *client.Client, 
instance *solr.SolrCloud) (*SecurityConfig, error) {
        // user has the option of providing a secret with credentials the 
operator should use to make requests to Solr
+       if instance.Spec.SolrSecurity.BasicAuthSecret != "" {
+               return reconcileForBasicAuthWithUserProvidedSecret(ctx, client, 
instance)
+       } else {
+               // user didn't provide a basicAuthSecret, so it's invalid for 
them to provide a security.json as the operator
+               // has no way of authenticating to Solr with a user provided 
security.json w/o also having the credentials in a secret
+               if instance.Spec.SolrSecurity.BootstrapSecurityJson != nil {
+                       return nil, fmt.Errorf("invalid basic auth config, you 
must also provide the 'basicAuthSecret' when providing your own 
'security.json'")
+               }
+               return reconcileForBasicAuthWithBootstrappedSecurityJson(ctx, 
client, instance)
+       }
+}
+
+// Create a "bootstrap" security.json with basic auth enabled with the 
"admin", "solr", and "k8s" users having random passwords
+func reconcileForBasicAuthWithBootstrappedSecurityJson(ctx context.Context, 
client *client.Client, instance *solr.SolrCloud) (*SecurityConfig, error) {
+       reader := *client
+
        sec := instance.Spec.SolrSecurity
+       security := &SecurityConfig{SolrSecurity: sec}
 
-       if sec.AuthenticationType != solr.Basic {
-               return nil, fmt.Errorf("%s not supported! Only 'Basic' 
authentication is supported by the Solr operator",
-                       instance.Spec.SolrSecurity.AuthenticationType)
-       }
+       // We're supplying a secret with random passwords and a default 
security.json
+       // since we randomly generate the passwords, we need to lookup the 
secret first and only create if not exist
+       basicAuthSecret := &corev1.Secret{}
+       err := reader.Get(ctx, types.NamespacedName{Name: 
instance.BasicAuthSecretName(), Namespace: instance.Namespace}, basicAuthSecret)
+       if err != nil && errors.IsNotFound(err) {
+               authSecret, bootstrapSecret := 
generateBasicAuthSecretWithBootstrap(instance)
 
-       // TODO: we shouldn't need to enforce this restriction?!?
-       //
-       // for now, we don't support 'solrSecurity.probesRequireAuth=true' and 
custom probe paths,
-       // so make the user fix that so there are no surprises later
-       if sec.ProbesRequireAuth && 
instance.Spec.CustomSolrKubeOptions.PodOptions != nil {
-               for _, path := range GetCustomProbePaths(instance) {
-                       if path != DefaultProbePath {
-                               return nil, fmt.Errorf(
-                                       "custom probe path %s not supported 
when 'solrSecurity.probesRequireAuth=true'; must use 
'solrSecurity.probesRequireAuth=false' when using custom probe endpoints", path)
-                       }
+               // take ownership of these secrets since we created them
+               if err := controllerutil.SetControllerReference(instance, 
authSecret, reader.Scheme()); err != nil {
+                       return nil, err
                }
-       }
-
-       if sec.BasicAuthSecret != "" {
-               // the user supplied their own basic auth secret, make sure it 
exists and has the expected keys
-               if err := reader.Get(ctx, types.NamespacedName{Name: 
sec.BasicAuthSecret, Namespace: instance.Namespace}, basicAuthSecret); err != 
nil {
+               if err := controllerutil.SetControllerReference(instance, 
bootstrapSecret, reader.Scheme()); err != nil {
                        return nil, err
                }
-
-               err := ValidateBasicAuthSecret(basicAuthSecret)
+               err = reader.Create(ctx, authSecret)
+               if err != nil {
+                       return nil, err
+               }
+               err = reader.Create(ctx, bootstrapSecret)
                if err != nil {
                        return nil, err
                }
 
-               // since the user supplied us with a basic auth secret, we're 
assuming they're also bootstrapping the security.json,
-               // so there is no bootstrap secret in this case
+               // supply the bootstrap security.json to the initContainer via 
a simple BASE64 encoding env var
+               security.SecurityJson = 
string(bootstrapSecret.Data[SecurityJsonFile])
+               basicAuthSecret = authSecret
+       }
 
-       } else {
-               // We're supplying a secret with random passwords and a default 
security.json
-               // since we randomly generate the passwords, we need to lookup 
the secret first and only create if not exist
-               err := reader.Get(ctx, types.NamespacedName{Name: 
instance.BasicAuthSecretName(), Namespace: instance.Namespace}, basicAuthSecret)
-               if err != nil && errors.IsNotFound(err) {
-                       authSecret, bootstrapSecret := 
generateBasicAuthSecretWithBootstrap(instance)
-
-                       // take ownership of these secrets since we created them
-                       if err := 
controllerutil.SetControllerReference(instance, authSecret, reader.Scheme()); 
err != nil {
-                               return nil, err
-                       }
-                       if err := 
controllerutil.SetControllerReference(instance, bootstrapSecret, 
reader.Scheme()); err != nil {
-                               return nil, err
-                       }
-                       err = reader.Create(ctx, authSecret)
-                       if err != nil {
-                               return nil, err
-                       }
-                       err = reader.Create(ctx, bootstrapSecret)
-                       if err != nil {
-                               return nil, err
-                       }
+       if err != nil {
+               return nil, err
+       }
+       security.CredentialsSecret = basicAuthSecret
 
-                       // supply the bootstrap security.json to the 
initContainer via a simple BASE64 encoding env var
+       if security.SecurityJson == "" {
+               // the bootstrap secret already exists, so just stash the 
security.json needed for constructing initContainers
+               bootstrapSecret := &corev1.Secret{}
+               err = reader.Get(ctx, types.NamespacedName{Name: 
instance.SecurityBootstrapSecretName(), Namespace: instance.Namespace}, 
bootstrapSecret)
+               if err != nil {
+                       if !errors.IsNotFound(err) {
+                               return nil, err
+                       } // else perhaps the user deleted it after security 
was bootstrapped ... this is ok but may trigger a restart on the STS
+               } else {
+                       // stash this so we can configure the setup-zk 
initContainer to bootstrap the security.json in ZK
                        security.SecurityJson = 
string(bootstrapSecret.Data[SecurityJsonFile])
-                       basicAuthSecret = authSecret
+                       security.SecurityJsonSrc = &corev1.EnvVarSource{
+                               SecretKeyRef: &corev1.SecretKeySelector{
+                                       LocalObjectReference: 
corev1.LocalObjectReference{Name: bootstrapSecret.Name}, Key: SecurityJsonFile}}
                }
+       }
+
+       return security, nil
+}
+
+// Basic auth but the user provides a secret containing credentials the 
operator should use to make requests to a secure Solr
+func reconcileForBasicAuthWithUserProvidedSecret(ctx context.Context, client 
*client.Client, instance *solr.SolrCloud) (*SecurityConfig, error) {
+       reader := *client
+
+       sec := instance.Spec.SolrSecurity
+       security := &SecurityConfig{SolrSecurity: sec}
 
+       // the user supplied their own basic auth secret, make sure it exists 
and has the expected keys
+       basicAuthSecret := &corev1.Secret{}
+       if err := reader.Get(ctx, types.NamespacedName{Name: 
sec.BasicAuthSecret, Namespace: instance.Namespace}, basicAuthSecret); err != 
nil {
+               return nil, err
+       }
+
+       err := ValidateBasicAuthSecret(basicAuthSecret)
+       if err != nil {
+               return nil, err
+       }
+       security.CredentialsSecret = basicAuthSecret
+
+       // is there a user-provided security.json in a secret?
+       // in this config, we don't need to enforce the user providing a 
security.json as they can bootstrap the security.json however they want
+       if sec.BootstrapSecurityJson != nil {
+               securityJson, err := loadSecurityJsonFromSecret(ctx, client, 
sec.BootstrapSecurityJson, instance.Namespace)
                if err != nil {
                        return nil, err
                }
-
-               security.BasicAuthSecret = basicAuthSecret
-
-               if security.SecurityJson == "" {
-                       // the bootstrap secret already exists, so just stash 
the security.json needed for constructing initContainers
-                       bootstrapSecret := &corev1.Secret{}
-                       err = reader.Get(ctx, types.NamespacedName{Name: 
instance.SecurityBootstrapSecretName(), Namespace: instance.Namespace}, 
bootstrapSecret)
-                       if err != nil {
-                               if !errors.IsNotFound(err) {
-                                       return nil, err
-                               } // else perhaps the user deleted it after 
security was bootstrapped ... this is ok but may trigger a restart on the STS
-                       } else {
-                               // stash this so we can configure the setup-zk 
initContainer to bootstrap the security.json in ZK
-                               security.SecurityJson = 
string(bootstrapSecret.Data[SecurityJsonFile])
-                       }
-               }
-       }
+               security.SecurityJson = securityJson
+               security.SecurityJsonSrc = &corev1.EnvVarSource{SecretKeyRef: 
sec.BootstrapSecurityJson}
+       } // else no user-provided secret, no sweat for us
 
        return security, nil
 }
@@ -146,8 +180,9 @@ func enableSecureProbesOnSolrCloudStatefulSet(solrCloud 
*solr.SolrCloud, statefu
        mainContainer := &stateful.Spec.Template.Spec.Containers[0]
 
        // if probes require auth or Solr wants client auth (mTLS), need to 
invoke a command on the Solr pod for the probes
+       // but only Basic auth is supported for now
        mountPath := ""
-       if solrCloud.Spec.SolrSecurity != nil && 
solrCloud.Spec.SolrSecurity.ProbesRequireAuth {
+       if solrCloud.Spec.SolrSecurity != nil && 
solrCloud.Spec.SolrSecurity.ProbesRequireAuth && 
solrCloud.Spec.SolrSecurity.AuthenticationType == solr.Basic {
                vol, volMount := 
secureProbeVolumeAndMount(solrCloud.BasicAuthSecretName())
                if vol != nil {
                        stateful.Spec.Template.Spec.Volumes = 
append(stateful.Spec.Template.Spec.Volumes, *vol)
@@ -177,26 +212,31 @@ func cmdToPutSecurityJsonInZk() string {
        return fmt.Sprintf(cmd, scriptsDir, scriptsDir)
 }
 
-func (security *SecurityConfig) AuthHeader() map[string]string {
-       if security.BasicAuthSecret != nil {
-               return map[string]string{"Authorization": 
BasicAuthHeader(security.BasicAuthSecret)}
+// Add auth data to the supplied Context using secrets already resolved 
(stored in the SecurityConfig)
+func (security *SecurityConfig) AddAuthToContext(ctx context.Context) 
(context.Context, error) {
+       if security.SolrSecurity.AuthenticationType == solr.Basic {
+               return contextWithBasicAuthHeader(ctx, 
security.CredentialsSecret), nil
        }
-       return nil
+       return ctx, nil
 }
 
-func BasicAuthEnvVars(secretName string) []corev1.EnvVar {
-       lor := corev1.LocalObjectReference{Name: secretName}
-       usernameRef := &corev1.SecretKeySelector{LocalObjectReference: lor, 
Key: corev1.BasicAuthUsernameKey}
-       passwordRef := &corev1.SecretKeySelector{LocalObjectReference: lor, 
Key: corev1.BasicAuthPasswordKey}
-       return []corev1.EnvVar{
-               {Name: "BASIC_AUTH_USER", ValueFrom: 
&corev1.EnvVarSource{SecretKeyRef: usernameRef}},
-               {Name: "BASIC_AUTH_PASS", ValueFrom: 
&corev1.EnvVarSource{SecretKeyRef: passwordRef}},
+// Similar to security.AddAuthToContext but we need to lookup the secret 
containing the authn credentials first
+func AddAuthToContext(ctx context.Context, client *client.Client, solrSecurity 
*solr.SolrSecurityOptions, ns string) (context.Context, error) {
+       reader := *client
+       if solrSecurity.AuthenticationType == solr.Basic {
+               basicAuthSecret := &corev1.Secret{}
+               if err := reader.Get(ctx, types.NamespacedName{Name: 
solrSecurity.BasicAuthSecret, Namespace: ns}, basicAuthSecret); err != nil {
+                       return nil, err
+               }
+               return contextWithBasicAuthHeader(ctx, basicAuthSecret), nil
        }
+       return ctx, nil
 }
 
-func BasicAuthHeader(basicAuthSecret *corev1.Secret) string {
+func contextWithBasicAuthHeader(ctx context.Context, basicAuthSecret 
*corev1.Secret) context.Context {
        creds := fmt.Sprintf("%s:%s", 
basicAuthSecret.Data[corev1.BasicAuthUsernameKey], 
basicAuthSecret.Data[corev1.BasicAuthPasswordKey])
-       return "Basic " + b64.StdEncoding.EncodeToString([]byte(creds))
+       headerValue := "Basic " + b64.StdEncoding.EncodeToString([]byte(creds))
+       return context.WithValue(ctx, solr_api.HTTP_HEADERS_CONTEXT_KEY, 
map[string]string{"Authorization": headerValue})
 }
 
 func ValidateBasicAuthSecret(basicAuthSecret *corev1.Secret) error {
@@ -204,17 +244,19 @@ func ValidateBasicAuthSecret(basicAuthSecret 
*corev1.Secret) error {
                return fmt.Errorf("invalid secret type %v; user-provided secret 
%s must be of type: %v",
                        basicAuthSecret.Type, basicAuthSecret.Name, 
corev1.SecretTypeBasicAuth)
        }
+       return validateCredentialsSecretData(basicAuthSecret, solr.Basic, 
corev1.BasicAuthUsernameKey, corev1.BasicAuthPasswordKey)
+}
 
-       if _, ok := basicAuthSecret.Data[corev1.BasicAuthUsernameKey]; !ok {
-               return fmt.Errorf("%s key not found in user-provided basic-auth 
secret %s",
-                       corev1.BasicAuthUsernameKey, basicAuthSecret.Name)
+func validateCredentialsSecretData(credsSecret *corev1.Secret, authType 
solr.AuthenticationType, userKey string, passKey string) error {
+       if _, ok := credsSecret.Data[userKey]; !ok {
+               return fmt.Errorf("required key '%s' not found in user-provided 
%s auth secret %s",
+                       userKey, authType, credsSecret.Name)
        }
 
-       if _, ok := basicAuthSecret.Data[corev1.BasicAuthPasswordKey]; !ok {
-               return fmt.Errorf("%s key not found in user-provided basic-auth 
secret %s",
-                       corev1.BasicAuthPasswordKey, basicAuthSecret.Name)
+       if _, ok := credsSecret.Data[passKey]; !ok {
+               return fmt.Errorf("required key '%s' not found in user-provided 
%s auth secret %s",
+                       passKey, authType, credsSecret.Name)
        }
-
        return nil
 }
 
@@ -393,6 +435,16 @@ func secureProbeVolumeAndMount(secretName string) 
(*corev1.Volume, *corev1.Volum
        return vol, volMount
 }
 
+func BasicAuthEnvVars(secretName string) []corev1.EnvVar {
+       lor := corev1.LocalObjectReference{Name: secretName}
+       usernameRef := &corev1.SecretKeySelector{LocalObjectReference: lor, 
Key: corev1.BasicAuthUsernameKey}
+       passwordRef := &corev1.SecretKeySelector{LocalObjectReference: lor, 
Key: corev1.BasicAuthPasswordKey}
+       return []corev1.EnvVar{
+               {Name: "BASIC_AUTH_USER", ValueFrom: 
&corev1.EnvVarSource{SecretKeyRef: usernameRef}},
+               {Name: "BASIC_AUTH_PASS", ValueFrom: 
&corev1.EnvVarSource{SecretKeyRef: passwordRef}},
+       }
+}
+
 // When running with TLS and clientAuth=Need or if the probe endpoints require 
auth, we need to use a command instead of HTTP Get
 // This function builds the custom probe command and returns any associated 
volume / mounts needed for the auth secrets
 func useSecureProbe(solrCloud *solr.SolrCloud, probe *corev1.Probe, mountPath 
string) {
@@ -400,7 +452,7 @@ func useSecureProbe(solrCloud *solr.SolrCloud, probe 
*corev1.Probe, mountPath st
        // 
https://kubernetes.io/docs/concepts/configuration/secret/#environment-variables-are-not-updated-after-a-secret-update
        basicAuthOption := ""
        enableBasicAuth := ""
-       if solrCloud.Spec.SolrSecurity != nil && 
solrCloud.Spec.SolrSecurity.ProbesRequireAuth {
+       if solrCloud.Spec.SolrSecurity != nil && 
solrCloud.Spec.SolrSecurity.ProbesRequireAuth && 
solrCloud.Spec.SolrSecurity.AuthenticationType == solr.Basic {
                usernameFile := fmt.Sprintf("%s/%s", mountPath, 
corev1.BasicAuthUsernameKey)
                passwordFile := fmt.Sprintf("%s/%s", mountPath, 
corev1.BasicAuthPasswordKey)
                basicAuthOption = fmt.Sprintf("-Dbasicauth=$(cat %s):$(cat 
%s)", usernameFile, passwordFile)
@@ -431,3 +483,22 @@ func useSecureProbe(solrCloud *solr.SolrCloud, probe 
*corev1.Probe, mountPath st
                probe.TimeoutSeconds = 5
        }
 }
+
+// Called during reconcile to load the security.json from a user-supplied 
secret
+func loadSecurityJsonFromSecret(ctx context.Context, client *client.Client, 
securityJsonSecret *corev1.SecretKeySelector, ns string) (string, error) {
+       sec := &corev1.Secret{}
+       nn := types.NamespacedName{Name: securityJsonSecret.Name, Namespace: ns}
+       reader := *client
+       err := reader.Get(ctx, nn, sec)
+       if err != nil {
+               return "", err
+       }
+
+       securityJson, hasSecurityJson := sec.Data[securityJsonSecret.Key]
+       if !hasSecurityJson {
+               return "", fmt.Errorf("required key '%s' not found in the 
user-supplied secret %s",
+                       securityJsonSecret.Key, sec.Name)
+       }
+
+       return string(securityJson), nil
+}
diff --git a/controllers/util/solr_update_util.go 
b/controllers/util/solr_update_util.go
index 72f373d..5ce6038 100644
--- a/controllers/util/solr_update_util.go
+++ b/controllers/util/solr_update_util.go
@@ -18,6 +18,7 @@
 package util
 
 import (
+       "context"
        "fmt"
        solr "github.com/apache/solr-operator/api/v1beta1"
        "github.com/apache/solr-operator/controllers/util/solr_api"
@@ -93,7 +94,7 @@ func scheduleNextRestartWithTime(restartSchedule string, 
podTemplateAnnotations
 // TODO:
 //  - Think about caching this for ~250 ms? Not a huge need to send these 
requests milliseconds apart.
 //    - Might be too much complexity for very little gain.
-func DeterminePodsSafeToUpdate(cloud *solr.SolrCloud, outOfDatePods 
[]corev1.Pod, readyPods int, availableUpdatedPodCount int, 
outOfDatePodsNotStartedCount int, logger logr.Logger, httpHeaders 
map[string]string) (podsToUpdate []corev1.Pod, retryLater bool) {
+func DeterminePodsSafeToUpdate(ctx context.Context, cloud *solr.SolrCloud, 
outOfDatePods []corev1.Pod, readyPods int, availableUpdatedPodCount int, 
outOfDatePodsNotStartedCount int, logger logr.Logger) (podsToUpdate 
[]corev1.Pod, retryLater bool) {
        // Before fetching the cluster state, be sure that there is room to 
update at least 1 pod
        maxPodsUnavailable, unavailableUpdatedPodCount, maxPodsToUpdate := 
calculateMaxPodsToUpdate(cloud, len(outOfDatePods), 
outOfDatePodsNotStartedCount, availableUpdatedPodCount)
        if maxPodsToUpdate <= 0 {
@@ -106,13 +107,13 @@ func DeterminePodsSafeToUpdate(cloud *solr.SolrCloud, 
outOfDatePods []corev1.Pod
                if readyPods > 0 {
                        queryParams := url.Values{}
                        queryParams.Add("action", "CLUSTERSTATUS")
-                       err := solr_api.CallCollectionsApi(cloud, queryParams, 
httpHeaders, clusterResp)
+                       err := solr_api.CallCollectionsApi(ctx, cloud, 
queryParams, clusterResp)
                        if err == nil {
                                if hasError, apiErr := 
solr_api.CheckForCollectionsApiError("CLUSTERSTATUS", 
clusterResp.ResponseHeader); hasError {
                                        err = apiErr
                                } else {
                                        queryParams.Set("action", 
"OVERSEERSTATUS")
-                                       err = 
solr_api.CallCollectionsApi(cloud, queryParams, httpHeaders, overseerResp)
+                                       err = solr_api.CallCollectionsApi(ctx, 
cloud, queryParams, overseerResp)
                                        if hasError, apiErr := 
solr_api.CheckForCollectionsApiError("OVERSEERSTATUS", 
clusterResp.ResponseHeader); hasError {
                                                err = apiErr
                                        }
diff --git a/controllers/util/solr_util.go b/controllers/util/solr_util.go
index 9f41e22..c3b4a44 100644
--- a/controllers/util/solr_util.go
+++ b/controllers/util/solr_util.go
@@ -1027,11 +1027,7 @@ func generateZKInteractionInitContainer(solrCloud 
*solr.SolrCloud, solrCloudStat
        }
 
        if security != nil && security.SecurityJson != "" {
-               envVars = append(envVars, corev1.EnvVar{Name: "SECURITY_JSON", 
ValueFrom: &corev1.EnvVarSource{
-                       SecretKeyRef: &corev1.SecretKeySelector{
-                               LocalObjectReference: 
corev1.LocalObjectReference{Name: solrCloud.SecurityBootstrapSecretName()},
-                               Key:                  SecurityJsonFile}}})
-
+               envVars = append(envVars, corev1.EnvVar{Name: "SECURITY_JSON", 
ValueFrom: security.SecurityJsonSrc})
                cmd += cmdToPutSecurityJsonInZk()
        }
 
diff --git a/docs/solr-cloud/solr-cloud-crd.md 
b/docs/solr-cloud/solr-cloud-crd.md
index fb3bea9..67c2ba1 100644
--- a/docs/solr-cloud/solr-cloud-crd.md
+++ b/docs/solr-cloud/solr-cloud-crd.md
@@ -798,13 +798,12 @@ basic authentication with TLS to ensure credentials are 
never passed in clear te
 
 For background on Solr security, please refer to the [Reference 
Guide](https://solr.apache.org/guide) for your version of Solr.
 
-Basic authentication is the only authentication scheme supported by the Solr 
operator at this time. In general, you have 
-two basic options for configuring basic authentication with the Solr operator:
+The Solr operator only supports the `Basic` authentication scheme. In general, 
you have two primary options for configuring authentication with the Solr 
operator:
 1. Let the Solr operator bootstrap the `security.json` to configure *basic 
authentication* for Solr.
 2. Supply your own `security.json` to Solr, which must define a user account 
that the operator can use to make API requests to secured Solr pods.
 
-If you choose option 2, then you need to provide the credentials to the Solr 
operator using a Kubernetes [Basic Authentication 
Secret](https://kubernetes.io/docs/concepts/configuration/secret/#basic-authentication-secret).
-With option 1, the operator creates the Basic Authentication Secret for you.
+If you choose option 2, then you need to provide the credentials the Solr 
operator should use to make requests to Solr via a Kubernetes secret. 
+With option 1, the operator creates a Basic Authentication Secret for you, 
which contains the username and password for the `k8s-oper` user.
 
 ### Option 1: Bootstrap Security
 
@@ -1009,6 +1008,12 @@ The exporter also hits the `/admin/ping` endpoint for 
every collection, which re
         "collection": "*",
         "path": "/admin/ping"
       },
+      { 
+         "name": "k8s-zk", 
+         "role":"k8s", 
+         "collection": null, 
+         "path":"/admin/zookeeper/status" 
+      },
 ```
 The `"collection":"*"` setting indicates this path applies to all collections, 
which maps to endpoint `/collections/<COLL>/admin/ping` at runtime.
 
@@ -1016,9 +1021,33 @@ The initial authorization config grants the `read` 
permission to the `users` rol
 For instance, the `solr` user is mapped to the `users` role, so the `solr` 
user can send query requests only. 
 In general, please verify the initial authorization rules for each role before 
sharing user credentials.
 
-### Option 2: User-provided Basic Auth Secret
+### Option 2: User-provided `security.json` and credentials secret
+
+If users want full control over their cluster's security config, then they can 
provide the Solr `security.json` via a Secret and the credentials the operator 
should use
+to make requests to Solr in a Secret.
+
+#### Custom `security.json` Secret
+_Since v0.5.0_
+
+For full control over the Solr security configuration, supply a 
`security.json` in a Secret. The following example illustrates how to point the 
operator to a Secret containing a custom `security.json`:
 
-Alternatively, if users want full control over their cluster's security 
config, then they can provide a `kubernetes.io/basic-auth` secret containing 
the credentials for the user they want the operator to make API requests as:
+```yaml
+spec:
+  ...
+  solrSecurity:
+    authenticationType: Basic
+    bootstrapSecurityJson:
+      name: my-custom-security-json
+      key: security.json
+```
+For `Basic` authentication, if you don't supply a `security.json` Secret, then 
the operator assumes you are bootstrapping the security configuration via some 
other means.
+
+Refer to the example `security.json` shown in the Authorization section above 
to help you get started crafting your own custom configuration. 
+
+#### Basic Authentication 
+
+For `Basic` authentication, the supplied secret must be of type [Basic 
Authentication 
Secret](https://kubernetes.io/docs/concepts/configuration/secret/#basic-authentication-secret)
 and define both a `username` and `password`.
+ 
 ```yaml
 spec:
   ...
@@ -1026,7 +1055,7 @@ spec:
     authenticationType: Basic
     basicAuthSecret: user-provided-secret
 ```
-The supplied secret must be of type [Basic Authentication 
Secret](https://kubernetes.io/docs/concepts/configuration/secret/#basic-authentication-secret)
 and define both a `username` and `password`.
+
 Here is an example of how to define a basic auth secret using YAML:
 ```yaml
 apiVersion: v1
@@ -1041,25 +1070,23 @@ stringData:
 With this config, the operator will make API requests to secured Solr pods as 
the `k8s-oper` user. 
 _Note: be sure to use a stronger password for real deployments_
 
-If users supply their own basic auth secret, then the operator *does not* 
bootstrap the `security.json`; 
-the reasoning is that if you're supplying your own basic auth credentials then 
you're also assuming the responsibility for configuring the desired access for 
this user.
-
-Users need to ensure their `security.json` contains the user supplied in the 
`basicAuthSecret` with read access to:
+Users need to ensure their `security.json` contains the user supplied in the 
`basicAuthSecret` has read access to the following endpoints:
 ```
 /admin/info/system
 /admin/info/health
 /admin/collections
 /admin/metrics
 /admin/ping (for collection="*")
+/admin/zookeeper/status
 ```
 _Tip: see the authorization rules defined by the default `security.json` as a 
guide for configuring access for the operator user_
 
-#### Changing the Password
+##### Changing the Password
 
 If you change the password for the user configured in your `basicAuthSecret` 
using the Solr security API, then you **must** update the secret with the new 
password or the operator will be locked out.
 Also, changing the password for this user in the K8s secret will not update 
Solr! You're responsible for changing the password in both places.
 
-### Prometheus Exporter with Basic Auth
+##### Prometheus Exporter with Basic Auth
 
 If you enable basic auth for your SolrCloud cluster, then you need to point 
the Prometheus exporter at the basic auth secret; 
 refer to [Prometheus Exporter with Basic 
Auth](../solr-prometheus-exporter/README.md#prometheus-exporter-with-basic-auth)
 for more details.
diff --git a/helm/solr-operator/Chart.yaml b/helm/solr-operator/Chart.yaml
index c9f87cd..a2da167 100644
--- a/helm/solr-operator/Chart.yaml
+++ b/helm/solr-operator/Chart.yaml
@@ -141,6 +141,13 @@ annotations:
           url: https://github.com/apache/solr-operator/pull/350
         - name: Topology Spread Constraints Documentation
           url: 
https://kubernetes.io/docs/concepts/workloads/pods/pod-topology-spread-constraints/
+    - kind: added
+      description: Ability to bootstrap security configuration from a 
security.json in a user-supplied secret
+      links:
+        - name: Github Issue
+          url: https://github.com/apache/solr-operator/issues/355
+        - name: Github PR
+          url: https://github.com/apache/solr-operator/pull/356
   artifacthub.io/images: |
     - name: solr-operator
       image: apache/solr-operator:v0.5.0-prerelease
diff --git a/helm/solr-operator/crds/crds.yaml 
b/helm/solr-operator/crds/crds.yaml
index 43a9551..68a1e75 100644
--- a/helm/solr-operator/crds/crds.yaml
+++ b/helm/solr-operator/crds/crds.yaml
@@ -7029,6 +7029,21 @@ spec:
                   basicAuthSecret:
                     description: "Secret (kubernetes.io/basic-auth) containing 
credentials the operator should use for API requests to secure Solr pods. If 
you provide this secret, then the operator assumes you've also configured your 
own security.json file and uploaded it to Solr. If you change the password for 
this user using the Solr security API, then you *must* update the secret with 
the new password or the operator will be  locked out of Solr and API requests 
will fail, ultimately  [...]
                     type: string
+                  bootstrapSecurityJson:
+                    description: Configure a user-provided security.json from 
a secret to allow for advanced security config. If not specified, the operator 
bootstraps a security.json with basic auth enabled. This is a bootstrapping 
config only; once Solr is initialized, the security config should be managed by 
the security API.
+                    properties:
+                      key:
+                        description: The key of the secret to select from.  
Must be a valid secret key.
+                        type: string
+                      name:
+                        description: 'Name of the referent. More info: 
https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names 
TODO: Add other useful fields. apiVersion, kind, uid?'
+                        type: string
+                      optional:
+                        description: Specify whether the Secret or its key 
must be defined
+                        type: boolean
+                    required:
+                    - key
+                    type: object
                   probesRequireAuth:
                     description: Flag to indicate if the configured HTTP 
endpoint(s) used for the probes require authentication; defaults to false. If 
you set to true, then probes will use a local command on the main container to 
hit the secured endpoints with credentials sourced from an env var instead of 
HTTP directly.
                     type: boolean
diff --git a/helm/solr/README.md b/helm/solr/README.md
index 4647587..75a22b0 100644
--- a/helm/solr/README.md
+++ b/helm/solr/README.md
@@ -95,6 +95,8 @@ The command removes the SolrCloud resource, and then 
Kubernetes will garbage col
 | solrOptions.security.authenticationType | string | `""` | Type of 
authentication to use for Solr |
 | solrOptions.security.basicAuthSecret | string | `""` | Name of Secret in the 
same namespace that stores the basicAuth information for the Solr user |
 | solrOptions.security.probesRequireAuth | boolean | | Whether the probes for 
the SolrCloud pod require auth |
+| solrOptions.security.bootstrapSecurityJson.name | string | | Name of a 
Secret in the same namespace that stores a user-provided `security.json` to 
bootstrap the Solr security config |
+| solrOptions.security.bootstrapSecurityJson.key | string | | Key holding the 
user-provided `security.json` in the bootstrap security Secret |
 | updateStrategy.method | string | `"Managed"` | The method for conducting 
updates of Solr pods. Either `Managed`, `StatefulSet` or `Manual`. See the 
[docs](https://apache.github.io/solr-operator/docs/solr-cloud/solr-cloud-crd.html#update-strategy)
 for more information |
 | updateStrategy.managedUpdate.maxPodsUnavailable | int-or-string | `"25%"` | 
The number of Solr pods in a Solr Cloud that are allowed to be unavailable 
during the rolling restart. Either a static number, or a percentage 
representing the percentage of total pods requested for the statefulSet. |
 | updateStrategy.managedUpdate.maxShardReplicasUnavailable | int-or-string | 
`1` | The number of replicas for each shard allowed to be unavailable during 
the restart. Either a static number, or a percentage representing the 
percentage of the number of replicas for a shard. |
diff --git a/helm/solr/values.yaml b/helm/solr/values.yaml
index f422820..bd6c2cc 100644
--- a/helm/solr/values.yaml
+++ b/helm/solr/values.yaml
@@ -64,6 +64,9 @@ solrOptions:
     # authenticationType: Basic
     # basicAuthSecret: secret-name
     # probesRequireAuth: false
+    # bootstrapSecurityJson:
+    #   name: my-custom-security-json-secret
+    #   key: security.json
 
 
 # Specify how the SolrCloud should be addressable

Reply via email to