Skip to content

Commit

Permalink
Merge pull request #14513 from wallyworld/secret-rotation
Browse files Browse the repository at this point in the history
#14513

Update the secret rotation infrastructure to work as per the spec. A few other changes were needed as well. Because the unit agent tracks the secret revision before the rotate hook runs, a new api call was added to fetch this info - but because this was also used for secret-ids outout and will also be used later for secret-get metadata, the api call was made to fit those use cases. And thus secret-ids was also updated.
We will also need a expiry watcher later, so the guts of the rotation watcher was made generic to fit that too.

## Checklist

- [X] Code style: imports ordered, good names, simple structure, etc
- [X] Comments saying why design decisions were made
- [X] Go unit tests, with comments saying what you're testing
- ~[ ] [Integration tests](https://github.com/juju/juju/tree/develop/tests), with comments saying what you're testing~
- ~[ ] [doc.go](https://discourse.charmhub.io/t/readme-in-packages/451) added or updated in changed packages~

## QA steps

I hacked the hourly rotate policy to set the rotation time to 3 minutes
deploy a charm with a secret-rotated hook
```
#!/bin/bash
echo "secret-rotate"
juju-log "secret-rotate uri=$JUJU_SECRET_ID label=$JUJU_SECRET_LABEL"
secret-update $JUJU_SECRET_ID data2=foobar
exit 0
```
Add a secret and update its rotation policy. Check that no rotations happen until the policy is set.
```
juju exec --unit ubuntu/0 "secret-add data=foo"
secret:cc2qophp7r7234v7clh0
juju exec --unit ubuntu/0 "secret-update secret:cc2qophp7r7234v7clh0 --rotate hourly"
```

After a few minutes, check debug-log to see the secret was rotated
```
unit-ubuntu-0: 14:46:42 INFO unit.ubuntu/0.juju-log secret-rotate uri=secret:cc2qophp7r7234v7clh0 label=
```
Also check the hook was run
```
juju show-status-log ubuntu/0
Time Type Status Message
24 Aug 2022 14:43:10+10:00 juju-unit allocating 
24 Aug 2022 14:43:10+10:00 workload waiting waiting for machine
24 Aug 2022 14:43:10+10:00 workload waiting installing agent
24 Aug 2022 14:43:10+10:00 workload waiting agent initializing
24 Aug 2022 14:43:10+10:00 workload maintenance installing charm software
24 Aug 2022 14:43:10+10:00 juju-unit executing running install hook
24 Aug 2022 14:43:11+10:00 juju-unit executing running leader-elected hook
24 Aug 2022 14:43:11+10:00 juju-unit executing running config-changed hook
24 Aug 2022 14:43:11+10:00 juju-unit executing running start hook
24 Aug 2022 14:43:11+10:00 workload unknown 
24 Aug 2022 14:43:11+10:00 juju-unit idle 
24 Aug 2022 14:43:18+10:00 juju-unit executing running action juju-exec
24 Aug 2022 14:43:18+10:00 juju-unit idle 
24 Aug 2022 14:43:41+10:00 juju-unit executing running action juju-exec
24 Aug 2022 14:43:41+10:00 juju-unit idle 
24 Aug 2022 14:46:42+10:00 juju-unit executing running secret-rotate hook for secret:cc2qophp7r7234v7clh0
24 Aug 2022 14:46:42+10:00 juju-unit idle 
24 Aug 2022 14:49:42+10:00 juju-unit executing running secret-rotate hook for secret:cc2qophp7r7234v7clh0
24 Aug 2022 14:49:42+10:00 juju-unit idle 
```
And the revisions were updated
```
juju show-secret --revisions secret:cc2qophp7r7234v7clh0
cc2qophp7r7234v7clh0:
 revision: 3
 rotation: hourly
 rotates: 2022-08-24T04:52:42Z
 owner: ubuntu
 created: 2022-08-24T04:43:18Z
 updated: 2022-08-24T04:49:42Z
 revisions:
 - revision: 1
 created: 2022-08-24T04:43:18Z
 updated: 2022-08-24T04:43:18Z
 - revision: 2
 created: 2022-08-24T04:46:42Z
 updated: 2022-08-24T04:46:42Z
 - revision: 3
 created: 2022-08-24T04:49:42Z
 updated: 2022-08-24T04:49:42Z
```
  • Loading branch information
jujubot committed Aug 26, 2022
2 parents 66c6b28 + bd41a3d commit d137d8c
Show file tree
Hide file tree
Showing 63 changed files with 2,656 additions and 814 deletions.
172 changes: 148 additions & 24 deletions api/agent/secretsmanager/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,6 @@
package secretsmanager

import (
"time"

"github.com/juju/errors"
"github.com/juju/names/v4"

Expand Down Expand Up @@ -67,6 +65,57 @@ func (c *Client) Create(cfg *secrets.SecretConfig, ownerTag names.Tag, value sec
return uri, nil
}

// Update updates an existing secret value and/or config like rotate interval.
func (c *Client) Update(uri *secrets.URI, cfg *secrets.SecretConfig, value secrets.SecretValue) error {
var data secrets.SecretData
if value != nil {
data = value.EncodedValues()
if len(data) == 0 {
data = nil
}
}

var results params.ErrorResults

arg := params.UpdateSecretArg{
URI: uri.String(),
UpsertSecretArg: params.UpsertSecretArg{
RotatePolicy: cfg.RotatePolicy,
ExpireTime: cfg.ExpireTime,
Description: cfg.Description,
Label: cfg.Label,
Params: cfg.Params,
Data: data,
},
}
if err := c.facade.FacadeCall("UpdateSecrets", params.UpdateSecretArgs{
Args: []params.UpdateSecretArg{arg},
}, &results); err != nil {
return errors.Trace(err)
}
return results.OneError()
}

// Remove removes the specified secret.
func (c *Client) Remove(uri *secrets.URI) error {
args := params.SecretURIArgs{
Args: []params.SecretURIArg{{URI: uri.String()}},
}
var results params.ErrorResults
err := c.facade.FacadeCall("RemoveSecrets", args, &results)
if err != nil {
return errors.Trace(err)
}
if len(results.Results) != 1 {
return errors.Errorf("expected 1 result, got %d", len(results.Results))
}
result := results.Results[0]
if result.Error != nil {
return result.Error
}
return nil
}

// GetValue returns the value of a secret.
func (c *Client) GetValue(uri *secrets.URI, label string, update, peek bool) (secrets.SecretValue, error) {
arg := params.GetSecretValueArg{
Expand Down Expand Up @@ -117,8 +166,8 @@ func (c *Client) WatchSecretsChanges(unitName string) (watcher.StringsWatcher, e

// SecretRevisionInfo holds info used to read a secret vale.
type SecretRevisionInfo struct {
Revision int
Label string
LatestRevision int
Label string
}

// GetLatestSecretsRevisionInfo returns the current revision and labels for secrets consumed
Expand Down Expand Up @@ -147,38 +196,43 @@ func (c *Client) GetLatestSecretsRevisionInfo(unitName string, uris []string) (m
return nil, errors.Annotatef(err, "finding latest info for secret %q", uris[i])
}
info[uris[i]] = SecretRevisionInfo{
Revision: latest.Revision,
Label: latest.Label,
LatestRevision: latest.Revision,
Label: latest.Label,
}
}
return info, err
}

// SecretIds returns the caller's secret ids and their labels.
func (c *Client) SecretIds() (map[*secrets.URI]string, error) {
var results params.SecretIdResults
err := c.facade.FacadeCall("GetSecretIds", nil, &results)
// SecretMetadata returns metadata for the caller's secrets.
func (c *Client) SecretMetadata() ([]secrets.SecretMetadata, error) {
var results params.ListSecretResults
err := c.facade.FacadeCall("GetSecretMetadata", nil, &results)
if err != nil {
return nil, errors.Trace(err)
}
result := make(map[*secrets.URI]string)
if results.Error != nil {
return nil, apiservererrors.RestoreError(results.Error)
}
for id, info := range results.Result {
uri, err := secrets.ParseURI(id)
var result []secrets.SecretMetadata
for _, info := range results.Results {
uri, err := secrets.ParseURI(info.URI)
if err != nil {
return nil, errors.NotValidf("secret URI %q", id)
return nil, errors.NotValidf("secret URI %q", info.URI)
}
result[uri] = info.Label
result = append(result, secrets.SecretMetadata{
URI: uri,
Description: info.Description,
Label: info.Label,
RotatePolicy: secrets.RotatePolicy(info.RotatePolicy),
LatestRevision: info.LatestRevision,
LatestExpireTime: info.LatestExpireTime,
NextRotateTime: info.NextRotateTime,
})
}
return result, nil
}

// WatchSecretsRotationChanges returns a watcher which serves changes to
// secrets rotation config for any secrets managed by the specified owner.
func (c *Client) WatchSecretsRotationChanges(ownerTag string) (watcher.SecretRotationWatcher, error) {
var results params.SecretRotationWatchResults
func (c *Client) WatchSecretsRotationChanges(ownerTag string) (watcher.SecretTriggerWatcher, error) {
var results params.SecretTriggerWatchResults
args := params.Entities{
Entities: []params.Entity{{Tag: ownerTag}},
}
Expand All @@ -197,8 +251,8 @@ func (c *Client) WatchSecretsRotationChanges(ownerTag string) (watcher.SecretRot
return w, nil
}

// SecretRotated records when a secret was last rotated.
func (c *Client) SecretRotated(uri string, when time.Time) error {
// SecretRotated records the outcome of rotating a secret.
func (c *Client) SecretRotated(uri string, oldRevision int) error {
secretUri, err := secrets.ParseURI(uri)
if err != nil {
return errors.Trace(err)
Expand All @@ -207,8 +261,8 @@ func (c *Client) SecretRotated(uri string, when time.Time) error {
var results params.ErrorResults
args := params.SecretRotatedArgs{
Args: []params.SecretRotatedArg{{
URI: secretUri.String(),
When: when,
URI: secretUri.String(),
OriginalRevision: oldRevision,
}},
}
err = c.facade.FacadeCall("SecretsRotated", args, &results)
Expand All @@ -224,3 +278,73 @@ func (c *Client) SecretRotated(uri string, when time.Time) error {
}
return nil
}

// SecretRevokeGrantArgs holds the args used to grant or revoke access to a secret.
// To grant access, specify one of ApplicationName or UnitName, plus optionally RelationId.
// To revoke access, specify one of ApplicationName or UnitName.
type SecretRevokeGrantArgs struct {
ApplicationName *string
UnitName *string
RelationKey *string
Role secrets.SecretRole
}

// Grant grants access to the specified secret.
func (c *Client) Grant(uri *secrets.URI, p *SecretRevokeGrantArgs) error {
args := grantRevokeArgsToParams(p, uri)
var results params.ErrorResults
err := c.facade.FacadeCall("SecretsGrant", args, &results)
if err != nil {
return errors.Trace(err)
}
if len(results.Results) != 1 {
return errors.Errorf("expected 1 result, got %d", len(results.Results))
}
result := results.Results[0]
if result.Error != nil {
return result.Error
}
return nil
}

func grantRevokeArgsToParams(p *SecretRevokeGrantArgs, secretUri *secrets.URI) params.GrantRevokeSecretArgs {
var subjectTag, scopeTag string
if p.ApplicationName != nil {
subjectTag = names.NewApplicationTag(*p.ApplicationName).String()
}
if p.UnitName != nil {
subjectTag = names.NewUnitTag(*p.UnitName).String()
}
if p.RelationKey != nil {
scopeTag = names.NewRelationTag(*p.RelationKey).String()
} else {
scopeTag = subjectTag
}
args := params.GrantRevokeSecretArgs{
Args: []params.GrantRevokeSecretArg{{
URI: secretUri.String(),
ScopeTag: scopeTag,
SubjectTags: []string{subjectTag},
Role: string(p.Role),
}},
}
return args
}

// Revoke revokes access to the specified secret.
func (c *Client) Revoke(uri *secrets.URI, p *SecretRevokeGrantArgs) error {
args := grantRevokeArgsToParams(p, uri)
var results params.ErrorResults
err := c.facade.FacadeCall("SecretsRevoke", args, &results)
if err != nil {
return errors.Trace(err)
}
if len(results.Results) != 1 {
return errors.Errorf("expected 1 result, got %d", len(results.Results))
}
result := results.Results[0]
if result.Error != nil {
return result.Error
}
return nil
}

0 comments on commit d137d8c

Please sign in to comment.