Skip to content

Commit

Permalink
Add support for using MS Graph instead of AAD (#67)
Browse files Browse the repository at this point in the history
* Use the MS Graph API for atomic add/remove password operations

Azure Active Directory Graph API, now deprecated, does not provide
support for atomically creating/removing passwords on an application. As
a result, there is a race conditions that can occur when creds are being
created for roles configured with an existing service principal that
is configured on multiple mounts or across multiple Vault clusters.

Unfortunately,
[`Azure/azure-sdk-for-go`](https://github.com/Azure/azure-sdk-for-go)
does not yet offer a MS Graph API client, therefore, this PR utilizes
[`Azure/go-autorest`](https://github.com/Azure/go-autorest) to construct
a client the same as
[`Azure/azure-sdk-for-go`](https://github.com/Azure/azure-sdk-for-go).

This changeset preserves using the AAD Graph API by default but provides
a mount configuration option for toggling to the new MS Graph API. This
is because the two APIs require different API permissions. This allows
users to upgrade to the new plugin version and then switch to the new
API.

Additionally, although using the MS Graph API is a net benefit, it
itself has reliability issues when handling multiple requests in
parallel. More details can be found in
https://github.com/mdgreenfield/microsoft-graph-api-reliability and I am
working with Microsoft to try to get some of these reliability issues
resolved.

Fixes #58

Co-authored-by: Matt Greenfield <matt.greenfield@datadoghq.com>
Co-authored-by: Jason O'Donnell <2160810+jasonodonnell@users.noreply.github.com>
  • Loading branch information
3 people committed Sep 24, 2021
1 parent 34e122b commit 58a0345
Show file tree
Hide file tree
Showing 20 changed files with 1,757 additions and 666 deletions.
62 changes: 62 additions & 0 deletions api/api.go
@@ -0,0 +1,62 @@
package api

import (
"context"

"github.com/Azure/azure-sdk-for-go/profiles/latest/authorization/mgmt/authorization"
"github.com/Azure/go-autorest/autorest"
"github.com/Azure/go-autorest/autorest/date"
)

// AzureProvider is an interface to access underlying Azure Client objects and supporting services.
// Where practical the original function signature is preserved. Client provides higher
// level operations atop AzureProvider.
type AzureProvider interface {
ApplicationsClient
GroupsClient
ServicePrincipalClient

CreateRoleAssignment(
ctx context.Context,
scope string,
roleAssignmentName string,
parameters authorization.RoleAssignmentCreateParameters) (authorization.RoleAssignment, error)
DeleteRoleAssignmentByID(ctx context.Context, roleID string) (authorization.RoleAssignment, error)

ListRoleDefinitions(ctx context.Context, scope string, filter string) ([]authorization.RoleDefinition, error)
GetRoleDefinitionByID(ctx context.Context, roleID string) (result authorization.RoleDefinition, err error)
}

type ApplicationsClient interface {
GetApplication(ctx context.Context, applicationObjectID string) (result ApplicationResult, err error)
CreateApplication(ctx context.Context, displayName string) (result ApplicationResult, err error)
DeleteApplication(ctx context.Context, applicationObjectID string) (autorest.Response, error)
AddApplicationPassword(ctx context.Context, applicationObjectID string, displayName string, endDateTime date.Time) (result PasswordCredentialResult, err error)
RemoveApplicationPassword(ctx context.Context, applicationObjectID string, keyID string) (result autorest.Response, err 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"`
}

type PasswordCredentialResult struct {
autorest.Response `json:"-"`

PasswordCredential
}

type ApplicationResult struct {
autorest.Response `json:"-"`

AppID *string `json:"appId,omitempty"`
ID *string `json:"id,omitempty"`
PasswordCredentials []*PasswordCredential `json:"passwordCredentials,omitempty"`
}
147 changes: 147 additions & 0 deletions api/application_aad.go
@@ -0,0 +1,147 @@
package api

import (
"context"
"errors"
"fmt"
"strings"
"time"

"github.com/Azure/azure-sdk-for-go/services/graphrbac/1.6/graphrbac"
"github.com/Azure/go-autorest/autorest"
"github.com/Azure/go-autorest/autorest/date"
"github.com/Azure/go-autorest/autorest/to"
"github.com/hashicorp/errwrap"
"github.com/hashicorp/go-uuid"
)

type ActiveDirectoryApplicationClient struct {
Client *graphrbac.ApplicationsClient
Passwords Passwords
}

func (a *ActiveDirectoryApplicationClient) GetApplication(ctx context.Context, applicationObjectID string) (result ApplicationResult, err error) {
app, err := a.Client.Get(ctx, applicationObjectID)
if err != nil {
return ApplicationResult{}, err
}

return ApplicationResult{
AppID: app.AppID,
ID: app.ObjectID,
}, nil
}

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

app, err := a.Client.Create(ctx, graphrbac.ApplicationCreateParameters{
AvailableToOtherTenants: to.BoolPtr(false),
DisplayName: to.StringPtr(displayName),
Homepage: to.StringPtr(appURL),
IdentifierUris: to.StringSlicePtr([]string{appURL}),
})
if err != nil {
return ApplicationResult{}, err
}

return ApplicationResult{
AppID: app.AppID,
ID: app.ObjectID,
}, nil
}

func (a *ActiveDirectoryApplicationClient) DeleteApplication(ctx context.Context, applicationObjectID string) (autorest.Response, error) {
return a.Client.Delete(ctx, applicationObjectID)
}

func (a *ActiveDirectoryApplicationClient) AddApplicationPassword(ctx context.Context, applicationObjectID string, displayName string, endDateTime date.Time) (result PasswordCredentialResult, err error) {
keyID, err := uuid.GenerateUUID()
if err != nil {
return PasswordCredentialResult{}, err
}

// Key IDs are not secret, and they're a convenient way for an operator to identify Vault-generated
// passwords. These must be UUIDs, so the three leading bytes will be used as an indicator.
keyID = "ffffff" + keyID[6:]

password, err := a.Passwords.Generate(ctx)
if err != nil {
return PasswordCredentialResult{}, err
}

now := date.Time{Time: time.Now().UTC()}
cred := graphrbac.PasswordCredential{
StartDate: &now,
EndDate: &endDateTime,
KeyID: to.StringPtr(keyID),
Value: to.StringPtr(password),
}

// Load current credentials
resp, err := a.Client.ListPasswordCredentials(ctx, applicationObjectID)
if err != nil {
return PasswordCredentialResult{}, errwrap.Wrapf("error fetching credentials: {{err}}", err)
}
curCreds := *resp.Value

// Add and save credentials
curCreds = append(curCreds, cred)

if _, err := a.Client.UpdatePasswordCredentials(ctx, applicationObjectID,
graphrbac.PasswordCredentialsUpdateParameters{
Value: &curCreds,
},
); err != nil {
if strings.Contains(err.Error(), "size of the object has exceeded its limit") {
err = errors.New("maximum number of Application passwords reached")
}
return PasswordCredentialResult{}, errwrap.Wrapf("error updating credentials: {{err}}", err)
}

return PasswordCredentialResult{
PasswordCredential: PasswordCredential{
DisplayName: to.StringPtr(displayName),
StartDate: &now,
EndDate: &endDateTime,
KeyID: to.StringPtr(keyID),
SecretText: to.StringPtr(password),
},
}, nil
}

func (a *ActiveDirectoryApplicationClient) RemoveApplicationPassword(ctx context.Context, applicationObjectID string, keyID string) (result autorest.Response, err error) {
// Load current credentials
resp, err := a.Client.ListPasswordCredentials(ctx, applicationObjectID)
if err != nil {
return autorest.Response{}, errwrap.Wrapf("error fetching credentials: {{err}}", err)
}
curCreds := *resp.Value

// Remove credential
found := false
for i := range curCreds {
if to.String(curCreds[i].KeyID) == keyID {
curCreds[i] = curCreds[len(curCreds)-1]
curCreds = curCreds[:len(curCreds)-1]
found = true
break
}
}

// KeyID is not present, so nothing to do
if !found {
return autorest.Response{}, nil
}

// Save new credentials list
if _, err := a.Client.UpdatePasswordCredentials(ctx, applicationObjectID,
graphrbac.PasswordCredentialsUpdateParameters{
Value: &curCreds,
},
); err != nil {
return autorest.Response{}, errwrap.Wrapf("error updating credentials: {{err}}", err)
}

return autorest.Response{}, nil
}

0 comments on commit 58a0345

Please sign in to comment.