github.com/juju/juju@v0.0.0-20240430160146-1752b71fcf00/secrets/backend.go (about)

     1  // Copyright 2022 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  package secrets
     5  
     6  import (
     7  	"context"
     8  
     9  	"github.com/juju/errors"
    10  
    11  	"github.com/juju/juju/core/secrets"
    12  	"github.com/juju/juju/secrets/provider"
    13  )
    14  
    15  // PermissionDenied is returned when an api fails due to a permission issue.
    16  const PermissionDenied = errors.ConstError("permission denied")
    17  
    18  // secretsClient wraps a Juju secrets manager client.
    19  // If a backend is specified, the secret content is managed
    20  // by the backend instead of being stored in the Juju database.
    21  type secretsClient struct {
    22  	jujuAPI JujuAPIClient
    23  }
    24  
    25  // For testing.
    26  var (
    27  	GetBackend = getBackend
    28  )
    29  
    30  func getBackend(cfg *provider.ModelBackendConfig) (provider.SecretsBackend, error) {
    31  	p, err := provider.Provider(cfg.BackendType)
    32  	if err != nil {
    33  		return nil, errors.Trace(err)
    34  	}
    35  	return p.NewBackend(cfg)
    36  }
    37  
    38  // NewClient returns a new secret client configured to use the specified
    39  // secret backend as a content backend.
    40  func NewClient(jujuAPI JujuAPIClient) (*secretsClient, error) {
    41  	c := &secretsClient{
    42  		jujuAPI: jujuAPI,
    43  	}
    44  	return c, nil
    45  }
    46  
    47  // GetBackend returns the secrets backend for the specified ID and the model's current active backend ID.
    48  func (c *secretsClient) GetBackend(backendID *string, forDrain bool) (provider.SecretsBackend, string, error) {
    49  	if forDrain {
    50  		cfg, activeID, err := c.jujuAPI.GetBackendConfigForDrain(backendID)
    51  		if err != nil {
    52  			return nil, "", errors.Trace(err)
    53  		}
    54  		b, err := GetBackend(cfg)
    55  		if err != nil {
    56  			return nil, "", errors.Trace(err)
    57  		}
    58  		return b, activeID, nil
    59  	}
    60  	info, err := c.jujuAPI.GetSecretBackendConfig(backendID)
    61  	if err != nil {
    62  		return nil, "", errors.Trace(err)
    63  	}
    64  	want := info.ActiveID
    65  	if backendID != nil {
    66  		want = *backendID
    67  	}
    68  	cfg, ok := info.Configs[want]
    69  	if !ok {
    70  		return nil, "", errors.Errorf("secret backend %q missing from config", want)
    71  	}
    72  	b, err := GetBackend(&cfg)
    73  	return b, info.ActiveID, errors.Trace(err)
    74  }
    75  
    76  // GetContent implements Client.
    77  func (c *secretsClient) GetContent(uri *secrets.URI, label string, refresh, peek bool) (secrets.SecretValue, error) {
    78  	lastBackendID := ""
    79  	for {
    80  		content, backendCfg, wasDraining, err := c.jujuAPI.GetContentInfo(uri, label, refresh, peek)
    81  		if err != nil {
    82  			return nil, errors.Trace(err)
    83  		}
    84  		if err = content.Validate(); err != nil {
    85  			return nil, errors.Trace(err)
    86  		}
    87  		if content.ValueRef == nil {
    88  			return content.SecretValue, nil
    89  		}
    90  
    91  		backendID := content.ValueRef.BackendID
    92  		backend, err := GetBackend(backendCfg)
    93  		if err != nil {
    94  			return nil, errors.Trace(err)
    95  		}
    96  		val, err := backend.GetContent(context.TODO(), content.ValueRef.RevisionID)
    97  		if err == nil || !errors.Is(err, errors.NotFound) || lastBackendID == backendID {
    98  			return val, errors.Trace(err)
    99  		}
   100  		lastBackendID = backendID
   101  		// Secret may have been drained to the active backend.
   102  		if wasDraining {
   103  			continue
   104  		}
   105  		return nil, errors.Trace(err)
   106  	}
   107  }
   108  
   109  // GetRevisionContent implements Client.
   110  func (c *secretsClient) GetRevisionContent(uri *secrets.URI, revision int) (secrets.SecretValue, error) {
   111  	content, _, _, err := c.jujuAPI.GetRevisionContentInfo(uri, revision, false)
   112  	if err != nil {
   113  		return nil, errors.Trace(err)
   114  	}
   115  	if err = content.Validate(); err != nil {
   116  		return nil, errors.Trace(err)
   117  	}
   118  	if content.ValueRef == nil {
   119  		return content.SecretValue, nil
   120  	}
   121  
   122  	backendID := content.ValueRef.BackendID
   123  	backend, _, err := c.GetBackend(&backendID, false)
   124  	if err != nil {
   125  		return nil, errors.Trace(err)
   126  	}
   127  	return backend.GetContent(context.TODO(), content.ValueRef.RevisionID)
   128  }
   129  
   130  // SaveContent implements Client.
   131  func (c *secretsClient) SaveContent(uri *secrets.URI, revision int, value secrets.SecretValue) (secrets.ValueRef, error) {
   132  	activeBackend, activeBackendID, err := c.GetBackend(nil, false)
   133  	if err != nil {
   134  		if errors.Is(err, errors.NotFound) {
   135  			return secrets.ValueRef{}, errors.NotSupportedf("saving secret content to external backend")
   136  		}
   137  		return secrets.ValueRef{}, errors.Trace(err)
   138  	}
   139  	revId, err := activeBackend.SaveContent(context.TODO(), uri, revision, value)
   140  	if err != nil {
   141  		return secrets.ValueRef{}, errors.Trace(err)
   142  	}
   143  	return secrets.ValueRef{
   144  		BackendID:  activeBackendID,
   145  		RevisionID: revId,
   146  	}, nil
   147  }
   148  
   149  // DeleteContent implements Client.
   150  func (c *secretsClient) DeleteContent(uri *secrets.URI, revision int) error {
   151  	lastBackendID := ""
   152  	for {
   153  		content, backendCfg, wasDraining, err := c.jujuAPI.GetRevisionContentInfo(uri, revision, true)
   154  		if err != nil {
   155  			return errors.Trace(err)
   156  		}
   157  		if content.ValueRef == nil {
   158  			return nil
   159  		}
   160  
   161  		backendID := content.ValueRef.BackendID
   162  		backend, err := GetBackend(backendCfg)
   163  		if err != nil {
   164  			return errors.Trace(err)
   165  		}
   166  		err = backend.DeleteContent(context.TODO(), content.ValueRef.RevisionID)
   167  		if err == nil || !errors.Is(err, errors.NotFound) || lastBackendID == backendID {
   168  			return errors.Trace(err)
   169  		}
   170  		lastBackendID = backendID
   171  		// Secret may have been drained to the active backend.
   172  		if wasDraining {
   173  			continue
   174  		}
   175  		return errors.Trace(err)
   176  	}
   177  }
   178  
   179  // DeleteExternalContent implements Client.
   180  func (c *secretsClient) DeleteExternalContent(ref secrets.ValueRef) error {
   181  	backend, _, err := c.GetBackend(&ref.BackendID, false)
   182  	if err != nil {
   183  		return errors.Trace(err)
   184  	}
   185  	err = backend.DeleteContent(context.TODO(), ref.RevisionID)
   186  	if errors.Is(err, errors.NotFound) {
   187  		return nil
   188  	}
   189  	return errors.Trace(err)
   190  }