Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add rotate-root endpoint #70

Merged
merged 40 commits into from Oct 28, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
ebce851
Remove bare returns
pcman312 Sep 27, 2021
d6bb52e
Readability cleanup
pcman312 Sep 27, 2021
6d07ce0
Remove errwrap
pcman312 Sep 27, 2021
b6addec
Make tests happy again
pcman312 Sep 27, 2021
ebb0325
Add rotate-root endpoint
pcman312 Oct 4, 2021
41bd7eb
Use correct response value
pcman312 Oct 5, 2021
56227f5
Fix merge failure
pcman312 Oct 5, 2021
59884a2
Add additional AAD warnings; Respond to code review
pcman312 Oct 7, 2021
5916c4b
Fix test
pcman312 Oct 7, 2021
714aff4
Don't pass config as a pointer so it gets a copy
pcman312 Oct 7, 2021
c3615a8
Fix expiration date logic; fix inverted warning logic
pcman312 Oct 8, 2021
5eb9e4c
Minor code review tweaks
pcman312 Oct 13, 2021
4085253
Move expiration to config
pcman312 Oct 13, 2021
938122f
Don't error if there isn't an error
pcman312 Oct 13, 2021
e5cc93a
Update the config & remove old passwords in the WAL
pcman312 Oct 13, 2021
b46951d
Return default_expiration on config get
pcman312 Oct 13, 2021
23fadb9
Return expiration from GET config
pcman312 Oct 13, 2021
8532464
Update path_rotate_root.go
jasonodonnell Oct 18, 2021
bf9c235
Update per review
jasonodonnell Oct 20, 2021
a693813
Rebase
jasonodonnell Oct 20, 2021
77ae610
Fix test
jasonodonnell Oct 20, 2021
9d5d3ae
Revert "Rebase"
jasonodonnell Oct 20, 2021
699e815
Remove named returns
jasonodonnell Oct 20, 2021
d5b7a4b
Update per review
jasonodonnell Oct 20, 2021
5ba6ffc
Update path_config.go
jasonodonnell Oct 20, 2021
33a8e60
Update per review
jasonodonnell Oct 20, 2021
484eb75
Use periodicFunc, change wal
jasonodonnell Oct 25, 2021
17e7da4
Fix config test
jasonodonnell Oct 25, 2021
f76c855
Add expiration date, update logger
jasonodonnell Oct 25, 2021
23e34b4
Fix timer bug
jasonodonnell Oct 26, 2021
a3aae79
Change root expiration to timestamp
jasonodonnell Oct 26, 2021
7c9842f
Fix named returns
jasonodonnell Oct 26, 2021
8571e52
Update backend.go
jasonodonnell Oct 27, 2021
7ffdfd4
Update per feedback, add more tests
jasonodonnell Oct 27, 2021
d5eb293
Fix conflicts
jasonodonnell Oct 27, 2021
de93161
Add wal min age
jasonodonnell Oct 27, 2021
5ac5c26
Update mock
jasonodonnell Oct 27, 2021
ac58246
Update go version
jasonodonnell Oct 27, 2021
495d8d8
Revert "Update go version"
jasonodonnell Oct 27, 2021
7236969
Remove unused wal code
jasonodonnell Oct 28, 2021
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
18 changes: 8 additions & 10 deletions api/api.go
Expand Up @@ -2,6 +2,7 @@ package api

import (
"context"
"time"

"github.com/Azure/azure-sdk-for-go/profiles/latest/authorization/mgmt/authorization"
"github.com/Azure/go-autorest/autorest"
Expand Down Expand Up @@ -31,20 +32,17 @@ type ApplicationsClient interface {
GetApplication(ctx context.Context, applicationObjectID string) (ApplicationResult, error)
CreateApplication(ctx context.Context, displayName string) (ApplicationResult, error)
DeleteApplication(ctx context.Context, applicationObjectID string) error
AddApplicationPassword(ctx context.Context, applicationObjectID string, displayName string, endDateTime date.Time) (PasswordCredentialResult, error)
ListApplications(ctx context.Context, filter string) ([]ApplicationResult, error)
AddApplicationPassword(ctx context.Context, applicationObjectID string, displayName string, endDateTime time.Time) (PasswordCredentialResult, error)
RemoveApplicationPassword(ctx context.Context, applicationObjectID string, keyID string) error
}

type PasswordCredential struct {
DisplayName *string `json:"displayName"`
// StartDate - Start date.
StartDate *date.Time `json:"startDateTime,omitempty"`
// EndDate - End date.
EndDate *date.Time `json:"endDateTime,omitempty"`
// KeyID - Key ID.
KeyID *string `json:"keyId,omitempty"`
// Value - Key value.
SecretText *string `json:"secretText,omitempty"`
DisplayName *string `json:"displayName"`
StartDate *date.Time `json:"startDateTime,omitempty"`
EndDate *date.Time `json:"endDateTime,omitempty"`
KeyID *string `json:"keyId,omitempty"`
SecretText *string `json:"secretText,omitempty"`
}

type PasswordCredentialResult struct {
Expand Down
39 changes: 36 additions & 3 deletions api/application_aad.go
Expand Up @@ -31,6 +31,39 @@ func (a *ActiveDirectoryApplicationClient) GetApplication(ctx context.Context, a
}, nil
}

func (a *ActiveDirectoryApplicationClient) ListApplications(ctx context.Context, filter string) ([]ApplicationResult, error) {
resp, err := a.Client.List(ctx, filter)
if err != nil {
return nil, err
}

results := []ApplicationResult{}
for resp.NotDone() {
for _, app := range resp.Values() {
passCreds := []*PasswordCredential{}
for _, rawPC := range *app.PasswordCredentials {
pc := &PasswordCredential{
StartDate: rawPC.StartDate,
EndDate: rawPC.EndDate,
KeyID: rawPC.KeyID,
}
passCreds = append(passCreds, pc)
}
appResult := ApplicationResult{
AppID: app.AppID,
ID: app.ObjectID,
PasswordCredentials: passCreds,
}
results = append(results, appResult)
}
err = resp.NextWithContext(ctx)
if err != nil {
return results, fmt.Errorf("failed to get all results: %w", err)
}
}
return results, nil
}

func (a *ActiveDirectoryApplicationClient) CreateApplication(ctx context.Context, displayName string) (ApplicationResult, error) {
appURL := fmt.Sprintf("https://%s", displayName)

Expand Down Expand Up @@ -62,7 +95,7 @@ func (a *ActiveDirectoryApplicationClient) DeleteApplication(ctx context.Context
return nil
}

func (a *ActiveDirectoryApplicationClient) AddApplicationPassword(ctx context.Context, applicationObjectID string, displayName string, endDateTime date.Time) (PasswordCredentialResult, error) {
func (a *ActiveDirectoryApplicationClient) AddApplicationPassword(ctx context.Context, applicationObjectID string, displayName string, endDateTime time.Time) (PasswordCredentialResult, error) {
keyID, err := uuid.GenerateUUID()
if err != nil {
return PasswordCredentialResult{}, err
Expand All @@ -80,7 +113,7 @@ func (a *ActiveDirectoryApplicationClient) AddApplicationPassword(ctx context.Co
now := date.Time{Time: time.Now().UTC()}
cred := graphrbac.PasswordCredential{
StartDate: &now,
EndDate: &endDateTime,
EndDate: &date.Time{endDateTime},
KeyID: to.StringPtr(keyID),
Value: to.StringPtr(password),
}
Expand Down Expand Up @@ -110,7 +143,7 @@ func (a *ActiveDirectoryApplicationClient) AddApplicationPassword(ctx context.Co
PasswordCredential: PasswordCredential{
DisplayName: to.StringPtr(displayName),
StartDate: &now,
EndDate: &endDateTime,
EndDate: &date.Time{endDateTime},
KeyID: to.StringPtr(keyID),
SecretText: to.StringPtr(password),
},
Expand Down
39 changes: 31 additions & 8 deletions api/application_msgraph.go
Expand Up @@ -72,6 +72,31 @@ func (c *AppClient) GetApplication(ctx context.Context, applicationObjectID stri
return result, nil
}

type listApplicationsResponse struct {
jasonodonnell marked this conversation as resolved.
Show resolved Hide resolved
Value []ApplicationResult `json:"value"`
}

func (c *AppClient) ListApplications(ctx context.Context, filter string) ([]ApplicationResult, error) {
filterArgs := url.Values{}
if filter != "" {
filterArgs.Set("$filter", filter)
}
preparer := c.GetPreparer(
autorest.AsGet(),
autorest.WithPath(fmt.Sprintf("/v1.0/applications?%s", filterArgs.Encode())),
)
listAppResp := listApplicationsResponse{}
err := c.SendRequest(ctx, preparer,
azure.WithErrorUnlessStatusCode(http.StatusOK),
autorest.ByUnmarshallingJSON(&listAppResp),
)
if err != nil {
return nil, err
}

return listAppResp.Value, nil
}

// CreateApplication create a new Azure application object.
func (c *AppClient) CreateApplication(ctx context.Context, displayName string) (ApplicationResult, error) {
var result ApplicationResult
Expand All @@ -90,6 +115,7 @@ func (c *AppClient) CreateApplication(ctx context.Context, displayName string) (
result, err = c.createApplicationResponder(resp)
if err != nil {
return result, autorest.NewErrorWithError(err, "provider", "CreateApplication", resp, "Failure responding to request")

}

return result, nil
Expand Down Expand Up @@ -117,27 +143,24 @@ func (c *AppClient) DeleteApplication(ctx context.Context, applicationObjectID s
if err != nil {
jasonodonnell marked this conversation as resolved.
Show resolved Hide resolved
jasonodonnell marked this conversation as resolved.
Show resolved Hide resolved
return autorest.NewErrorWithError(err, "provider", "DeleteApplication", resp, "Failure responding to request")
}

return nil
}

func (c *AppClient) AddApplicationPassword(ctx context.Context, applicationObjectID string, displayName string, endDateTime date.Time) (PasswordCredentialResult, error) {
var result PasswordCredentialResult

req, err := c.addPasswordPreparer(ctx, applicationObjectID, displayName, endDateTime)
func (c *AppClient) AddApplicationPassword(ctx context.Context, applicationObjectID string, displayName string, endDateTime time.Time) (PasswordCredentialResult, error) {
req, err := c.addPasswordPreparer(ctx, applicationObjectID, displayName, date.Time{endDateTime})
if err != nil {
return PasswordCredentialResult{}, autorest.NewErrorWithError(err, "provider", "AddApplicationPassword", nil, "Failure preparing request")
}

resp, err := c.addPasswordSender(req)
if err != nil {
result = PasswordCredentialResult{
result := PasswordCredentialResult{
Response: autorest.Response{Response: resp},
}
return result, autorest.NewErrorWithError(err, "provider", "AddApplicationPassword", resp, "Failure sending request")
}

result, err = c.addPasswordResponder(resp)
result, err := c.addPasswordResponder(resp)
if err != nil {
return result, autorest.NewErrorWithError(err, "provider", "AddApplicationPassword", resp, "Failure responding to request")
}
Expand Down Expand Up @@ -487,7 +510,7 @@ func (c *AppClient) setPasswordForServicePrincipal(ctx context.Context, spID str
}
reqBody := map[string]interface{}{
"startDateTime": startDate.UTC().Format("2006-01-02T15:04:05Z"),
"endDateTime": startDate.UTC().Format("2006-01-02T15:04:05Z"),
"endDateTime": endDate.UTC().Format("2006-01-02T15:04:05Z"),
}

preparer := c.GetPreparer(
Expand Down
4 changes: 2 additions & 2 deletions api/groups_aad.go
Expand Up @@ -38,7 +38,7 @@ func (a ActiveDirectoryApplicationGroupsClient) RemoveGroupMember(ctx context.Co
return err
}

func (a ActiveDirectoryApplicationGroupsClient) GetGroup(ctx context.Context, objectID string) (result Group, err error) {
func (a ActiveDirectoryApplicationGroupsClient) GetGroup(ctx context.Context, objectID string) (Group, error) {
resp, err := a.Client.Get(ctx, objectID)
if err != nil {
return Group{}, err
Expand All @@ -57,7 +57,7 @@ func getGroupFromRBAC(resp graphrbac.ADGroup) Group {
return grp
}

func (a ActiveDirectoryApplicationGroupsClient) ListGroups(ctx context.Context, filter string) (result []Group, err error) {
func (a ActiveDirectoryApplicationGroupsClient) ListGroups(ctx context.Context, filter string) ([]Group, error) {
resp, err := a.Client.List(ctx, filter)
if err != nil {
return nil, err
Expand Down
2 changes: 1 addition & 1 deletion api/passwords.go
Expand Up @@ -20,7 +20,7 @@ type Passwords struct {
PolicyName string
}

func (p Passwords) Generate(ctx context.Context) (password string, err error) {
func (p Passwords) Generate(ctx context.Context) (string, error) {
if p.PolicyName == "" {
return base62.Random(PasswordLength)
}
Expand Down
4 changes: 2 additions & 2 deletions api/service_principals_aad.go
Expand Up @@ -17,13 +17,13 @@ type AADServicePrincipalsClient struct {
Passwords Passwords
}

func (c AADServicePrincipalsClient) CreateServicePrincipal(ctx context.Context, appID string, startDate time.Time, endDate time.Time) (id string, password string, err error) {
func (c AADServicePrincipalsClient) CreateServicePrincipal(ctx context.Context, appID string, startDate time.Time, endDate time.Time) (string, string, error) {
keyID, err := uuid.GenerateUUID()
if err != nil {
return "", "", err
}

password, err = c.Passwords.Generate(ctx)
password, err := c.Passwords.Generate(ctx)
if err != nil {
return "", "", err
}
Expand Down
85 changes: 80 additions & 5 deletions backend.go
Expand Up @@ -2,6 +2,7 @@ package azuresecrets

import (
"context"
"fmt"
"strings"
"sync"
"time"
Expand All @@ -22,7 +23,8 @@ type azureSecretBackend struct {

// Creating/deleting passwords against a single Application is a PATCH
// operation that must be locked per Application Object ID.
appLocks []*locksutil.LockEntry
appLocks []*locksutil.LockEntry
updatePassword bool
}

func Factory(ctx context.Context, conf *logical.BackendConfig) (logical.Backend, error) {
Expand All @@ -34,7 +36,9 @@ func Factory(ctx context.Context, conf *logical.BackendConfig) (logical.Backend,
}

func backend() *azureSecretBackend {
var b = azureSecretBackend{}
var b = azureSecretBackend{
updatePassword: true,
}

b.Backend = &framework.Backend{
Help: strings.TrimSpace(backendHelp),
Expand All @@ -48,6 +52,7 @@ func backend() *azureSecretBackend {
[]*framework.Path{
pathConfig(&b),
pathServicePrincipal(&b),
pathRotateRoot(&b),
},
),
Secrets: []*framework.Secret{
Expand All @@ -57,19 +62,89 @@ func backend() *azureSecretBackend {
BackendType: logical.TypeLogical,
Invalidate: b.invalidate,

WALRollback: b.walRollback,

// Role assignment can take up to a few minutes, so ensure we don't try
// to roll back during creation.
WALRollbackMinAge: 10 * time.Minute,
jasonodonnell marked this conversation as resolved.
Show resolved Hide resolved
}

WALRollback: b.walRollback,
PeriodicFunc: b.periodicFunc,
}
b.getProvider = newAzureProvider
b.appLocks = locksutil.CreateLocks()

return &b
}

func (b *azureSecretBackend) periodicFunc(ctx context.Context, sys *logical.Request) error {
b.Logger().Debug("starting periodic func")
if !b.updatePassword {
b.Logger().Debug("periodic func", "rotate-root", "no rotate-root update")
return nil
}

config, err := b.getConfig(ctx, sys.Storage)
if err != nil {
return err
}

// Password should be at least a minute old before we process it
if config.NewClientSecret == "" || (time.Since(config.NewClientSecretCreated) < time.Minute) {
return nil
}

b.Logger().Debug("periodic func", "rotate-root", "new password detected, swapping in storage")
client, err := b.getClient(ctx, sys.Storage)
if err != nil {
return err
}

apps, err := client.provider.ListApplications(ctx, fmt.Sprintf("appId eq '%s'", config.ClientID))
if err != nil {
return err
}

if len(apps) == 0 {
return fmt.Errorf("no application found")
}
if len(apps) > 1 {
return fmt.Errorf("multiple applications found - double check your client_id")
}

app := apps[0]

credsToDelete := []string{}
for _, cred := range app.PasswordCredentials {
if *cred.KeyID != config.NewClientSecretKeyID {
credsToDelete = append(credsToDelete, *cred.KeyID)
}
}

if len(credsToDelete) != 0 {
b.Logger().Debug("periodic func", "rotate-root", "removing old passwords from Azure")
err = removeApplicationPasswords(ctx, client.provider, *app.ID, credsToDelete...)
if err != nil {
return err
}
}

b.Logger().Debug("periodic func", "rotate-root", "updating config with new password")
config.ClientSecret = config.NewClientSecret
config.ClientSecretKeyID = config.NewClientSecretKeyID
config.RootPasswordExpirationDate = config.NewClientSecretExpirationDate
config.NewClientSecret = ""
config.NewClientSecretKeyID = ""
config.NewClientSecretCreated = time.Time{}

err = b.saveConfig(ctx, config, sys.Storage)
if err != nil {
return err
}

b.updatePassword = false

return nil
}

// reset clears the backend's cached client
// This is used when the configuration changes and a new client should be
// created with the updated settings.
Expand Down
9 changes: 7 additions & 2 deletions backend_test.go
Expand Up @@ -18,6 +18,11 @@ const (
defaultTestMaxTTL = 3600
)

var (
testClientID = "testClientId"
testClientSecret = "testClientSecret"
)

func getTestBackend(t *testing.T, initConfig bool) (*azureSecretBackend, logical.Storage) {
b := backend()

Expand All @@ -44,8 +49,8 @@ func getTestBackend(t *testing.T, initConfig bool) (*azureSecretBackend, logical
cfg := map[string]interface{}{
"subscription_id": generateUUID(),
"tenant_id": generateUUID(),
"client_id": "testClientId",
"client_secret": "testClientSecret",
"client_id": testClientID,
"client_secret": testClientSecret,
"environment": "AZURECHINACLOUD",
"ttl": defaultTestTTL,
"max_ttl": defaultTestMaxTTL,
Expand Down