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 }