| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610 |
- /*
- Copyright © 2025 ESO Maintainer Team
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
- https://www.apache.org/licenses/LICENSE-2.0
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
- */
- // Package beyondtrust provides a Password Safe secrets provider for External Secrets Operator.
- package beyondtrust
- import (
- "context"
- "encoding/json"
- "errors"
- "fmt"
- "net/url"
- "strings"
- "time"
- auth "github.com/BeyondTrust/go-client-library-passwordsafe/api/authentication"
- "github.com/BeyondTrust/go-client-library-passwordsafe/api/entities"
- "github.com/BeyondTrust/go-client-library-passwordsafe/api/logging"
- managedaccount "github.com/BeyondTrust/go-client-library-passwordsafe/api/managed_account"
- "github.com/BeyondTrust/go-client-library-passwordsafe/api/secrets"
- "github.com/BeyondTrust/go-client-library-passwordsafe/api/utils"
- "github.com/cenkalti/backoff/v4"
- v1 "k8s.io/api/core/v1"
- ctrl "sigs.k8s.io/controller-runtime"
- "sigs.k8s.io/controller-runtime/pkg/client"
- "sigs.k8s.io/controller-runtime/pkg/webhook/admission"
- esv1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1"
- esutils "github.com/external-secrets/external-secrets/runtime/esutils"
- "github.com/external-secrets/external-secrets/runtime/esutils/resolvers"
- )
- const (
- errNilStore = "nil store found"
- errMissingStoreSpec = "store is missing spec"
- errMissingProvider = "storeSpec is missing provider"
- errInvalidProvider = "invalid provider spec. Missing field in store %s"
- errInvalidHostURL = "invalid host URL"
- errNoSuchKeyFmt = "no such key in secret: %q"
- errInvalidRetrievalPath = "invalid retrieval path. Provide one path, separator and name"
- errNotImplemented = "not implemented"
- usernameFieldName = "username"
- folderNameFieldName = "folder_name"
- fileNameFieldName = "file_name"
- titleFieldName = "title"
- descriptionFieldName = "description"
- ownerIDFieldName = "owner_id"
- groupIDFieldName = "group_id"
- ownerTypeFieldName = "owner_type"
- secretTypeFieldName = "secret_type"
- secretTypeCredential = "CREDENTIAL"
- )
- var (
- errSecretRefAndValueConflict = errors.New("cannot specify both secret reference and value")
- errMissingSecretName = errors.New("must specify a secret name")
- errMissingSecretKey = errors.New("must specify a secret key")
- // ESOLogger is the logger instance for the Beyondtrust provider.
- ESOLogger = ctrl.Log.WithName("provider").WithName("beyondtrust")
- maxFileSecretSizeBytes = 5000000
- )
- // Provider is a Password Safe secrets provider implementing NewClient and ValidateStore for the esv1.Provider interface.
- type Provider struct {
- apiURL string
- retrievaltype string
- decrypt bool
- authenticate auth.AuthenticationObj
- log logging.LogrLogger
- separator string
- }
- // AuthenticatorInput is used to pass parameters to the getAuthenticator function.
- type AuthenticatorInput struct {
- Config *esv1.BeyondtrustProvider
- HTTPClientObj utils.HttpClientObj
- BackoffDefinition *backoff.ExponentialBackOff
- APIURL string
- APIVersion string
- ClientID string
- ClientSecret string
- APIKey string
- Logger *logging.LogrLogger
- RetryMaxElapsedTimeMinutes int
- }
- // Capabilities implements v1beta1.Provider.
- func (*Provider) Capabilities() esv1.SecretStoreCapabilities {
- return esv1.SecretStoreReadWrite
- }
- // Close implements v1beta1.SecretsClient.
- func (*Provider) Close(_ context.Context) error {
- return nil
- }
- // DeleteSecret implements v1beta1.SecretsClient.
- func (*Provider) DeleteSecret(_ context.Context, _ esv1.PushSecretRemoteRef) error {
- return errors.New(errNotImplemented)
- }
- // GetSecretMap implements v1beta1.SecretsClient.
- func (*Provider) GetSecretMap(_ context.Context, _ esv1.ExternalSecretDataRemoteRef) (map[string][]byte, error) {
- return make(map[string][]byte), errors.New(errNotImplemented)
- }
- // Validate implements v1beta1.SecretsClient.
- func (p *Provider) Validate() (esv1.ValidationResult, error) {
- timeout := 15 * time.Second
- clientURL := p.apiURL
- if err := esutils.NetworkValidate(clientURL, timeout); err != nil {
- ESOLogger.Error(err, "Network Validate", "clientURL:", clientURL)
- return esv1.ValidationResultError, err
- }
- return esv1.ValidationResultReady, nil
- }
- // SecretExists checks if a secret exists in the provider.
- func (p *Provider) SecretExists(_ context.Context, pushSecretRef esv1.PushSecretRemoteRef) (bool, error) {
- logger := logging.NewLogrLogger(&ESOLogger)
- secretObj, err := secrets.NewSecretObj(p.authenticate, logger, maxFileSecretSizeBytes, false)
- if err != nil {
- return false, err
- }
- _, err = secretObj.SearchSecretByTitleFlow(pushSecretRef.GetRemoteKey())
- if err == nil {
- return true, nil
- }
- return false, nil
- }
- // NewClient this is where we initialize the SecretClient and return it for the controller to use.
- func (p *Provider) NewClient(ctx context.Context, store esv1.GenericStore, kube client.Client, namespace string) (esv1.SecretsClient, error) {
- config := store.GetSpec().Provider.Beyondtrust
- logger := logging.NewLogrLogger(&ESOLogger)
- storeKind := store.GetKind()
- clientID, clientSecret, apiKey, err := loadCredentialsFromConfig(ctx, config, kube, namespace, storeKind)
- if err != nil {
- return nil, fmt.Errorf("error loading credentials: %w", err)
- }
- certificate, certificateKey, err := loadCertificateFromConfig(ctx, config, kube, namespace, storeKind)
- if err != nil {
- return nil, fmt.Errorf("error loading certificate: %w", err)
- }
- clientTimeOutInSeconds, separator, retryMaxElapsedTimeMinutes := getConfigValues(config)
- backoffDefinition := getBackoffDefinition(retryMaxElapsedTimeMinutes)
- params := utils.ValidationParams{
- ApiKey: apiKey,
- ClientID: clientID,
- ClientSecret: clientSecret,
- ApiUrl: &config.Server.APIURL,
- ApiVersion: config.Server.APIVersion,
- ClientTimeOutInSeconds: clientTimeOutInSeconds,
- Separator: &separator,
- VerifyCa: config.Server.VerifyCA,
- Logger: logger,
- Certificate: certificate,
- CertificateKey: certificateKey,
- RetryMaxElapsedTimeMinutes: &retryMaxElapsedTimeMinutes,
- MaxFileSecretSizeBytes: &maxFileSecretSizeBytes,
- }
- if err := validateInputs(params); err != nil {
- return nil, fmt.Errorf("error in Inputs: %w", err)
- }
- httpClient, err := utils.GetHttpClient(clientTimeOutInSeconds, config.Server.VerifyCA, certificate, certificateKey, logger)
- if err != nil {
- return nil, fmt.Errorf("error creating HTTP client: %w", err)
- }
- authenticatorInput := AuthenticatorInput{
- Config: config,
- HTTPClientObj: *httpClient,
- BackoffDefinition: backoffDefinition,
- APIURL: config.Server.APIURL,
- APIVersion: config.Server.APIVersion,
- ClientID: clientID,
- ClientSecret: clientSecret,
- APIKey: apiKey,
- Logger: logger,
- RetryMaxElapsedTimeMinutes: retryMaxElapsedTimeMinutes,
- }
- authenticate, err := getAuthenticator(authenticatorInput)
- if err != nil {
- return nil, fmt.Errorf("error authenticating: %w", err)
- }
- return &Provider{
- apiURL: config.Server.APIURL,
- retrievaltype: config.Server.RetrievalType,
- authenticate: *authenticate,
- log: *logger,
- separator: separator,
- decrypt: config.Server.Decrypt,
- }, nil
- }
- func loadCredentialsFromConfig(ctx context.Context, config *esv1.BeyondtrustProvider, kube client.Client, namespace, storeKind string) (string, string, string, error) {
- if config.Auth.APIKey != nil {
- apiKey, err := loadConfigSecret(ctx, config.Auth.APIKey, kube, namespace, storeKind)
- return "", "", apiKey, err
- }
- clientID, err := loadConfigSecret(ctx, config.Auth.ClientID, kube, namespace, storeKind)
- if err != nil {
- return "", "", "", fmt.Errorf("error loading clientID: %w", err)
- }
- clientSecret, err := loadConfigSecret(ctx, config.Auth.ClientSecret, kube, namespace, storeKind)
- if err != nil {
- return "", "", "", fmt.Errorf("error loading clientSecret: %w", err)
- }
- return clientID, clientSecret, "", nil
- }
- func loadCertificateFromConfig(ctx context.Context, config *esv1.BeyondtrustProvider, kube client.Client, namespace, storeKind string) (string, string, error) {
- if config.Auth.Certificate == nil || config.Auth.CertificateKey == nil {
- return "", "", nil
- }
- certificate, err := loadConfigSecret(ctx, config.Auth.Certificate, kube, namespace, storeKind)
- if err != nil {
- return "", "", fmt.Errorf("error loading Certificate: %w", err)
- }
- certificateKey, err := loadConfigSecret(ctx, config.Auth.CertificateKey, kube, namespace, storeKind)
- if err != nil {
- return "", "", fmt.Errorf("error loading Certificate Key: %w", err)
- }
- return certificate, certificateKey, nil
- }
- func getConfigValues(config *esv1.BeyondtrustProvider) (int, string, int) {
- clientTimeOutInSeconds := 45
- separator := "/"
- retryMaxElapsedTimeMinutes := 15
- if config.Server.ClientTimeOutSeconds != 0 {
- clientTimeOutInSeconds = config.Server.ClientTimeOutSeconds
- }
- if config.Server.Separator != "" {
- separator = config.Server.Separator
- }
- return clientTimeOutInSeconds, separator, retryMaxElapsedTimeMinutes
- }
- func getBackoffDefinition(retryMaxElapsedTimeMinutes int) *backoff.ExponentialBackOff {
- backoffDefinition := backoff.NewExponentialBackOff()
- backoffDefinition.InitialInterval = 1 * time.Second
- backoffDefinition.MaxElapsedTime = time.Duration(retryMaxElapsedTimeMinutes) * time.Minute
- backoffDefinition.RandomizationFactor = 0.5
- return backoffDefinition
- }
- func validateInputs(params utils.ValidationParams) error {
- return utils.ValidateInputs(params)
- }
- func getAuthenticator(input AuthenticatorInput) (*auth.AuthenticationObj, error) {
- parametersObj := auth.AuthenticationParametersObj{
- HTTPClient: input.HTTPClientObj,
- BackoffDefinition: input.BackoffDefinition,
- EndpointURL: input.APIURL,
- APIVersion: input.APIVersion,
- ApiKey: input.APIKey,
- Logger: input.Logger,
- RetryMaxElapsedTimeSeconds: input.RetryMaxElapsedTimeMinutes,
- }
- if input.Config.Auth.APIKey != nil {
- parametersObj.ApiKey = input.APIKey
- return auth.AuthenticateUsingApiKey(parametersObj)
- }
- parametersObj.ClientID = input.ClientID
- parametersObj.ClientSecret = input.ClientSecret
- return auth.Authenticate(parametersObj)
- }
- func loadConfigSecret(ctx context.Context, ref *esv1.BeyondTrustProviderSecretRef, kube client.Client, defaultNamespace, storeKind string) (string, error) {
- if ref.SecretRef == nil {
- return ref.Value, nil
- }
- if err := validateSecretRef(ref); err != nil {
- return "", err
- }
- return resolvers.SecretKeyRef(ctx, kube, storeKind, defaultNamespace, ref.SecretRef)
- }
- func validateSecretRef(ref *esv1.BeyondTrustProviderSecretRef) error {
- if ref.SecretRef != nil {
- if ref.Value != "" {
- return errSecretRefAndValueConflict
- }
- if ref.SecretRef.Name == "" {
- return errMissingSecretName
- }
- if ref.SecretRef.Key == "" {
- return errMissingSecretKey
- }
- }
- return nil
- }
- // GetAllSecrets retrieves all secrets from Beyondtrust.
- func (p *Provider) GetAllSecrets(_ context.Context, _ esv1.ExternalSecretFind) (map[string][]byte, error) {
- return nil, errors.New("GetAllSecrets not implemented")
- }
- // GetSecret reads the secret from the Password Safe server and returns it. The controller uses the value here to
- // create the Kubernetes secret.
- func (p *Provider) GetSecret(_ context.Context, ref esv1.ExternalSecretDataRemoteRef) ([]byte, error) {
- managedAccountType := !strings.EqualFold(p.retrievaltype, "SECRET")
- retrievalPaths := utils.ValidatePaths([]string{ref.Key}, managedAccountType, p.separator, &p.log)
- if len(retrievalPaths) != 1 {
- return nil, errors.New(errInvalidRetrievalPath)
- }
- retrievalPath := retrievalPaths[0]
- _, err := p.authenticate.GetPasswordSafeAuthentication()
- if err != nil {
- return nil, fmt.Errorf("error getting authentication: %w", err)
- }
- managedFetch := func() (string, error) {
- ESOLogger.Info("retrieve managed account value", "retrievalPath:", retrievalPath)
- manageAccountObj, _ := managedaccount.NewManagedAccountObj(p.authenticate, &p.log)
- return manageAccountObj.GetSecret(retrievalPath, p.separator)
- }
- unmanagedFetch := func() (string, error) {
- ESOLogger.Info("retrieve secrets safe value", "retrievalPath:", retrievalPath)
- secretObj, _ := secrets.NewSecretObj(p.authenticate, &p.log, maxFileSecretSizeBytes, p.decrypt)
- return secretObj.GetSecret(retrievalPath, p.separator)
- }
- fetch := unmanagedFetch
- if managedAccountType {
- fetch = managedFetch
- }
- returnSecret, err := fetch()
- if err != nil {
- if serr := p.authenticate.SignOut(); serr != nil {
- return nil, errors.Join(err, serr)
- }
- return nil, fmt.Errorf("error getting secret/managed account: %w", err)
- }
- return []byte(returnSecret), nil
- }
- // ValidateStore validates the store configuration to prevent unexpected errors.
- func (p *Provider) ValidateStore(store esv1.GenericStore) (admission.Warnings, error) {
- if store == nil {
- return nil, errors.New(errNilStore)
- }
- spec := store.GetSpec()
- if spec == nil {
- return nil, errors.New(errMissingStoreSpec)
- }
- if spec.Provider == nil {
- return nil, errors.New(errMissingProvider)
- }
- provider := spec.Provider.Beyondtrust
- if provider == nil {
- return nil, fmt.Errorf(errInvalidProvider, store.GetObjectMeta().String())
- }
- apiURL, err := url.Parse(provider.Server.APIURL)
- if err != nil {
- return nil, errors.New(errInvalidHostURL)
- }
- if provider.Auth.ClientID.SecretRef != nil {
- return nil, err
- }
- if provider.Auth.ClientSecret.SecretRef != nil {
- return nil, err
- }
- if apiURL.Host == "" {
- return nil, errors.New(errInvalidHostURL)
- }
- return nil, nil
- }
- // NewProvider creates a new Provider instance.
- func NewProvider() esv1.Provider {
- return &Provider{}
- }
- // ProviderSpec returns the provider specification for registration.
- func ProviderSpec() *esv1.SecretStoreProvider {
- return &esv1.SecretStoreProvider{
- Beyondtrust: &esv1.BeyondtrustProvider{},
- }
- }
- // MaintenanceStatus returns the maintenance status of the provider.
- func MaintenanceStatus() esv1.MaintenanceStatus {
- return esv1.MaintenanceStatusMaintained
- }
- // PushSecret implements v1beta1.SecretsClient.
- func (p *Provider) PushSecret(_ context.Context, secret *v1.Secret, psd esv1.PushSecretData) error {
- ESOLogger.Info("Pushing secret to BeyondTrust Password Safe")
- value, err := esutils.ExtractSecretData(psd, secret)
- if err != nil {
- return fmt.Errorf("extract secret data failed: %w", err)
- }
- secretValue := string(value)
- metadata := psd.GetMetadata()
- data, err := json.Marshal(metadata)
- if err != nil {
- return fmt.Errorf("Error getting metadata: %w", err)
- }
- var metaDataObject map[string]interface{}
- err = json.Unmarshal(data, &metaDataObject)
- if err != nil {
- return fmt.Errorf("Error in parameters: %w", err)
- }
- signAppinResponse, err := p.authenticate.GetPasswordSafeAuthentication()
- if err != nil {
- return fmt.Errorf("Error in authentication: %w", err)
- }
- err = p.CreateSecret(secretValue, metaDataObject, signAppinResponse)
- if err != nil {
- return fmt.Errorf("Error in creating the secret: %w", err)
- }
- return nil
- }
- // CreateSecret creates a secret in BeyondTrust Password Safe.
- func (p *Provider) CreateSecret(secret string, data map[string]interface{}, signAppinResponse entities.SignAppinResponse) error {
- logger := logging.NewLogrLogger(&ESOLogger)
- secretObj, err := secrets.NewSecretObj(p.authenticate, logger, maxFileSecretSizeBytes, false)
- if err != nil {
- return err
- }
- username := utils.GetStringField(data, usernameFieldName, "")
- folderName := utils.GetStringField(data, folderNameFieldName, "")
- fileName := utils.GetStringField(data, fileNameFieldName, "")
- title := utils.GetStringField(data, titleFieldName, "")
- description := utils.GetStringField(data, descriptionFieldName, "")
- ownerID := utils.GetIntField(data, ownerIDFieldName, 0)
- groupID := utils.GetIntField(data, groupIDFieldName, 0)
- ownerType := utils.GetStringField(data, ownerTypeFieldName, "")
- secretType := utils.GetStringField(data, secretTypeFieldName, secretTypeCredential)
- var notes string
- var urls []entities.UrlDetails
- var ownerDetailsOwnerID []entities.OwnerDetailsOwnerId
- var ownerDetailsGroupID []entities.OwnerDetailsGroupId
- _, ok := data["notes"]
- if ok {
- notes = data["notes"].(string)
- }
- _, ok = data["urls"]
- if ok {
- urls = utils.GetUrlsDetailsList(data)
- }
- ownerDetailsOwnerID = utils.GetOwnerDetailsOwnerIdList(data, signAppinResponse)
- ownerDetailsGroupID = utils.GetOwnerDetailsGroupIdList(data, groupID, signAppinResponse)
- secretDetailsConfig := entities.SecretDetailsBaseConfig{
- Title: title,
- Description: description,
- Urls: urls,
- Notes: notes,
- }
- var configMap map[string]interface{}
- switch strings.ToUpper(secretType) {
- case "CREDENTIAL":
- secretCredentialDetailsConfig30 := entities.SecretCredentialDetailsConfig30{
- SecretDetailsBaseConfig: secretDetailsConfig,
- Username: username,
- Password: secret,
- OwnerId: ownerID,
- OwnerType: ownerType,
- Owners: ownerDetailsOwnerID,
- }
- secretCredentialDetailsConfig31 := entities.SecretCredentialDetailsConfig31{
- SecretDetailsBaseConfig: secretDetailsConfig,
- Username: username,
- Password: secret,
- Owners: ownerDetailsGroupID,
- }
- configMap = map[string]interface{}{
- "3.0": secretCredentialDetailsConfig30,
- "3.1": secretCredentialDetailsConfig31,
- }
- case "FILE":
- secretFileDetailsConfig30 := entities.SecretFileDetailsConfig30{
- SecretDetailsBaseConfig: secretDetailsConfig,
- FileContent: secret,
- FileName: fileName,
- OwnerId: ownerID,
- OwnerType: ownerType,
- Owners: ownerDetailsOwnerID,
- }
- secretFileDetailsConfig31 := entities.SecretFileDetailsConfig31{
- SecretDetailsBaseConfig: secretDetailsConfig,
- FileContent: secret,
- FileName: fileName,
- Owners: ownerDetailsGroupID,
- }
- configMap = map[string]interface{}{
- "3.0": secretFileDetailsConfig30,
- "3.1": secretFileDetailsConfig31,
- }
- case "TEXT":
- secretTextDetailsConfig30 := entities.SecretTextDetailsConfig30{
- SecretDetailsBaseConfig: secretDetailsConfig,
- Text: secret,
- OwnerId: ownerID,
- OwnerType: ownerType,
- Owners: ownerDetailsOwnerID,
- }
- secretTextDetailsConfig31 := entities.SecretTextDetailsConfig31{
- SecretDetailsBaseConfig: secretDetailsConfig,
- Text: secret,
- Owners: ownerDetailsGroupID,
- }
- configMap = map[string]interface{}{
- "3.0": secretTextDetailsConfig30,
- "3.1": secretTextDetailsConfig31,
- }
- default:
- return fmt.Errorf("Unknown secret type")
- }
- secretDetails, exists := configMap[p.authenticate.ApiVersion]
- if !exists {
- return fmt.Errorf("unsupported API version: %v", &p.authenticate.ApiVersion)
- }
- _, err = secretObj.CreateSecretFlow(folderName, secretDetails)
- if err != nil {
- return err
- }
- ESOLogger.Info("Secret pushed to BeyondTrust Password Safe")
- return nil
- }
|