Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add support for using MS Graph instead of AAD (#67)
* 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
1 parent
34e122b
commit 58a0345
Showing
20 changed files
with
1,757 additions
and
666 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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"` | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 | ||
} |
Oops, something went wrong.