2021-09-07 22:06:22 -04:00
|
|
|
package plugin
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"context"
|
|
|
|
|
"errors"
|
|
|
|
|
"fmt"
|
|
|
|
|
"time"
|
|
|
|
|
|
|
|
|
|
"github.com/Azure/go-autorest/autorest"
|
|
|
|
|
"github.com/Azure/go-autorest/autorest/azure/auth"
|
|
|
|
|
"github.com/hashicorp/boundary/sdk/pbs/controller/api/resources/hostcatalogs"
|
|
|
|
|
hauth "github.com/manicminer/hamilton/auth"
|
|
|
|
|
"github.com/manicminer/hamilton/environments"
|
|
|
|
|
"github.com/manicminer/hamilton/msgraph"
|
|
|
|
|
"github.com/manicminer/hamilton/odata"
|
2021-11-10 13:59:21 -05:00
|
|
|
"github.com/mitchellh/mapstructure"
|
2021-09-07 22:06:22 -04:00
|
|
|
)
|
|
|
|
|
|
2021-11-10 13:59:21 -05:00
|
|
|
type AuthorizationInfo struct {
|
|
|
|
|
HamiltonConfig hauth.Config
|
|
|
|
|
AuthParams AuthParams
|
2021-09-07 22:06:22 -04:00
|
|
|
}
|
|
|
|
|
|
2021-11-10 13:59:21 -05:00
|
|
|
type Attributes struct {
|
|
|
|
|
SubscriptionId string `mapstructure:"subscription_id"`
|
|
|
|
|
ClientId string `mapstructure:"client_id"`
|
|
|
|
|
TenantId string `mapstructure:"tenant_id"`
|
|
|
|
|
DisableCredentialRotation bool `mapstructure:"disable_credential_rotation"`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type SecretData struct {
|
|
|
|
|
SecretValue string `mapstructure:"secret_value"`
|
|
|
|
|
SecretId string `mapstructure:"secret_id"`
|
|
|
|
|
CredsLastRotatedTime string `mapstructure:"creds_last_rotated_time"`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type AuthParams struct {
|
|
|
|
|
SubscriptionId string
|
|
|
|
|
ClientId string
|
|
|
|
|
ClientObjectId string
|
|
|
|
|
TenantId string
|
|
|
|
|
SecretValue string
|
|
|
|
|
SecretId string
|
|
|
|
|
CredsLastRotatedTime time.Time
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func getAuthorizationInfo(hc *hostcatalogs.HostCatalog) (*AuthorizationInfo, error) {
|
2021-09-07 22:06:22 -04:00
|
|
|
if hc == nil {
|
|
|
|
|
return nil, errors.New("host catalog is nil")
|
|
|
|
|
}
|
|
|
|
|
if hc.GetAttributes() == nil {
|
|
|
|
|
return nil, errors.New("host catalog attributes is nil")
|
|
|
|
|
}
|
2021-11-10 13:59:21 -05:00
|
|
|
if hc.GetSecrets() == nil {
|
|
|
|
|
return nil, errors.New("host catalog secret data is nil")
|
2021-09-07 22:06:22 -04:00
|
|
|
}
|
|
|
|
|
|
2021-11-10 13:59:21 -05:00
|
|
|
hcFields := hc.GetAttributes().AsMap()
|
|
|
|
|
var attrs Attributes
|
|
|
|
|
if err := mapstructure.Decode(hcFields, &attrs); err != nil {
|
|
|
|
|
return nil, fmt.Errorf("error decoding host catalog attribute fields: %w", err)
|
2021-09-07 22:06:22 -04:00
|
|
|
}
|
2021-11-10 13:59:21 -05:00
|
|
|
if attrs.SubscriptionId == "" {
|
|
|
|
|
return nil, errors.New("subscription id is empty")
|
2021-09-07 22:06:22 -04:00
|
|
|
}
|
2021-11-10 13:59:21 -05:00
|
|
|
if attrs.TenantId == "" {
|
|
|
|
|
return nil, errors.New("tenant id is empty")
|
2021-09-07 22:06:22 -04:00
|
|
|
}
|
|
|
|
|
|
2021-11-10 13:59:21 -05:00
|
|
|
pdFields := hc.GetSecrets().AsMap()
|
|
|
|
|
var secrets SecretData
|
|
|
|
|
if err := mapstructure.Decode(pdFields, &secrets); err != nil {
|
|
|
|
|
return nil, fmt.Errorf("error decoding host catalog persisted data fields: %w", err)
|
2021-09-07 22:06:22 -04:00
|
|
|
}
|
2021-11-10 13:59:21 -05:00
|
|
|
if secrets.SecretValue == "" {
|
|
|
|
|
return nil, errors.New("secret value is empty")
|
|
|
|
|
}
|
|
|
|
|
var lastRotatedTime time.Time
|
|
|
|
|
var err error
|
|
|
|
|
if secrets.CredsLastRotatedTime != "" {
|
|
|
|
|
lastRotatedTime, err = time.Parse(time.RFC3339Nano, secrets.CredsLastRotatedTime)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, fmt.Errorf("error parsing last rotated time: %w", err)
|
2021-09-07 22:06:22 -04:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2021-11-10 13:59:21 -05:00
|
|
|
return &AuthorizationInfo{
|
|
|
|
|
HamiltonConfig: hauth.Config{
|
|
|
|
|
Environment: environments.Global,
|
|
|
|
|
TenantID: attrs.TenantId,
|
|
|
|
|
ClientID: attrs.ClientId,
|
|
|
|
|
ClientSecret: secrets.SecretValue,
|
|
|
|
|
EnableClientSecretAuth: true,
|
|
|
|
|
},
|
|
|
|
|
AuthParams: AuthParams{
|
|
|
|
|
SubscriptionId: attrs.SubscriptionId,
|
|
|
|
|
ClientId: attrs.ClientId,
|
|
|
|
|
TenantId: attrs.TenantId,
|
|
|
|
|
SecretId: secrets.SecretId,
|
|
|
|
|
SecretValue: secrets.SecretValue,
|
|
|
|
|
CredsLastRotatedTime: lastRotatedTime,
|
|
|
|
|
},
|
|
|
|
|
}, nil
|
2021-09-07 22:06:22 -04:00
|
|
|
}
|
|
|
|
|
|
2021-11-10 13:59:21 -05:00
|
|
|
func (a *AuthorizationInfo) autorestAuthorizer() (autorest.Authorizer, error) {
|
|
|
|
|
credsConfig := auth.NewClientCredentialsConfig(
|
|
|
|
|
a.AuthParams.ClientId,
|
|
|
|
|
a.AuthParams.SecretValue,
|
|
|
|
|
a.AuthParams.TenantId,
|
|
|
|
|
)
|
|
|
|
|
authorizer, err := credsConfig.Authorizer()
|
2021-09-07 22:06:22 -04:00
|
|
|
if err != nil {
|
2021-11-10 13:59:21 -05:00
|
|
|
return nil, fmt.Errorf("error authorizing to Azure: %w", err)
|
2021-09-07 22:06:22 -04:00
|
|
|
}
|
2021-11-10 13:59:21 -05:00
|
|
|
if authorizer == nil {
|
|
|
|
|
return nil, errors.New("generated authorizer is nil")
|
2021-09-07 22:06:22 -04:00
|
|
|
}
|
2021-11-10 13:59:21 -05:00
|
|
|
return authorizer, nil
|
2021-09-07 22:06:22 -04:00
|
|
|
}
|
|
|
|
|
|
2021-11-10 13:59:21 -05:00
|
|
|
func (a *AuthorizationInfo) populateObjectId(ctx context.Context, opt ...Option) error {
|
|
|
|
|
if a.AuthParams.ClientObjectId != "" {
|
|
|
|
|
return nil
|
2021-09-07 22:06:22 -04:00
|
|
|
}
|
2021-11-10 13:59:21 -05:00
|
|
|
|
|
|
|
|
if a.AuthParams.ClientId == "" {
|
|
|
|
|
return errors.New("client id not known at object id lookup time")
|
|
|
|
|
}
|
|
|
|
|
// First, find the appId
|
|
|
|
|
aClient, err := getApplicationsClient(ctx, a)
|
2021-09-07 22:06:22 -04:00
|
|
|
if err != nil {
|
2021-11-10 13:59:21 -05:00
|
|
|
return fmt.Errorf("error getting application client: %w", err)
|
2021-09-07 22:06:22 -04:00
|
|
|
}
|
2021-11-10 13:59:21 -05:00
|
|
|
if aClient == nil {
|
|
|
|
|
return errors.New("applications client is nil when getting object id")
|
2021-09-07 22:06:22 -04:00
|
|
|
}
|
2021-11-10 13:59:21 -05:00
|
|
|
apps, _, err := aClient.List(ctx, odata.Query{
|
|
|
|
|
Filter: fmt.Sprintf("appId eq '%s'", a.AuthParams.ClientId),
|
|
|
|
|
})
|
2021-09-07 22:06:22 -04:00
|
|
|
if err != nil {
|
2021-11-10 13:59:21 -05:00
|
|
|
return fmt.Errorf("error listing applications to find objId: %w", err)
|
2021-09-07 22:06:22 -04:00
|
|
|
}
|
2021-11-10 13:59:21 -05:00
|
|
|
if apps == nil {
|
|
|
|
|
return errors.New("nil apps returned when listing to find objId")
|
|
|
|
|
}
|
|
|
|
|
if len(*apps) != 1 {
|
|
|
|
|
return fmt.Errorf("unexpected number of apps found, expected 1, found %d", len(*apps))
|
2021-09-07 22:06:22 -04:00
|
|
|
}
|
2021-11-10 13:59:21 -05:00
|
|
|
a.AuthParams.ClientObjectId = *(*apps)[0].DirectoryObject.ID
|
|
|
|
|
|
|
|
|
|
return nil
|
2021-09-07 22:06:22 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// rotateCredentials creates a new password, then uses a client with that new
|
|
|
|
|
// password to revoke the old.
|
|
|
|
|
//
|
|
|
|
|
// NOTE: the underlying auth config will be modified to use the new credentials!
|
2021-11-10 13:59:21 -05:00
|
|
|
func rotateCredential(ctx context.Context, authzInfo *AuthorizationInfo, opt ...Option) (*msgraph.PasswordCredential, error) {
|
|
|
|
|
if authzInfo == nil {
|
|
|
|
|
return nil, errors.New("empty authz info")
|
2021-09-07 22:06:22 -04:00
|
|
|
}
|
2021-11-10 13:59:21 -05:00
|
|
|
if authzInfo.AuthParams.SecretId == "" {
|
2021-09-07 22:06:22 -04:00
|
|
|
return nil, errors.New("missing original secret id")
|
|
|
|
|
}
|
|
|
|
|
|
2021-11-10 13:59:21 -05:00
|
|
|
// Ensure the object ID is set
|
|
|
|
|
if err := authzInfo.populateObjectId(ctx); err != nil {
|
2021-09-07 22:06:22 -04:00
|
|
|
return nil, fmt.Errorf("error fetching object id: %w", err)
|
|
|
|
|
}
|
|
|
|
|
|
2021-11-10 13:59:21 -05:00
|
|
|
newCred, err := addCredential(ctx, authzInfo)
|
2021-09-07 22:06:22 -04:00
|
|
|
if err != nil {
|
|
|
|
|
return nil, fmt.Errorf("error adding password: %w", err)
|
|
|
|
|
}
|
|
|
|
|
if newCred == nil {
|
|
|
|
|
return nil, errors.New("new credential is nil after adding")
|
|
|
|
|
}
|
|
|
|
|
if newCred.SecretText == nil {
|
|
|
|
|
return nil, errors.New("new credential secret text is nil after adding")
|
|
|
|
|
}
|
|
|
|
|
|
2021-11-10 13:59:21 -05:00
|
|
|
if err := removeCredential(ctx, authzInfo); err != nil {
|
2021-09-07 22:06:22 -04:00
|
|
|
return nil, fmt.Errorf("error removing previous credential: %w", err)
|
|
|
|
|
}
|
|
|
|
|
return newCred, nil
|
|
|
|
|
}
|
|
|
|
|
|
2021-11-10 13:59:21 -05:00
|
|
|
func addCredential(ctx context.Context, authzInfo *AuthorizationInfo, opt ...Option) (*msgraph.PasswordCredential, error) {
|
|
|
|
|
if authzInfo == nil {
|
|
|
|
|
return nil, errors.New("empty authz info")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Ensure the object ID is set
|
|
|
|
|
if err := authzInfo.populateObjectId(ctx); err != nil {
|
2021-09-07 22:06:22 -04:00
|
|
|
return nil, fmt.Errorf("error fetching object id: %w", err)
|
|
|
|
|
}
|
|
|
|
|
|
2021-11-10 13:59:21 -05:00
|
|
|
aClient, err := getApplicationsClient(ctx, authzInfo)
|
2021-09-07 22:06:22 -04:00
|
|
|
if err != nil {
|
|
|
|
|
return nil, fmt.Errorf("error getting application client: %w", err)
|
|
|
|
|
}
|
|
|
|
|
if aClient == nil {
|
|
|
|
|
return nil, errors.New("applications client is nil when adding credential")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Create the new password
|
|
|
|
|
displayName := fmt.Sprintf("boundary-rotated-%s", time.Now().Format(time.RFC3339))
|
2021-11-10 13:59:21 -05:00
|
|
|
newPass, _, err := aClient.AddPassword(ctx, authzInfo.AuthParams.ClientObjectId, msgraph.PasswordCredential{
|
2021-09-07 22:06:22 -04:00
|
|
|
DisplayName: &displayName,
|
|
|
|
|
})
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, fmt.Errorf("error creating new password: %w", err)
|
|
|
|
|
}
|
|
|
|
|
if newPass == nil {
|
|
|
|
|
return nil, errors.New("nil new password returned")
|
|
|
|
|
}
|
|
|
|
|
if newPass.KeyId == nil {
|
|
|
|
|
return nil, errors.New("nil key ID for new password")
|
|
|
|
|
}
|
|
|
|
|
if newPass.SecretText == nil {
|
|
|
|
|
return nil, errors.New("nil secret text for new password")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return newPass, nil
|
|
|
|
|
}
|
|
|
|
|
|
2021-11-10 13:59:21 -05:00
|
|
|
func removeCredential(ctx context.Context, authzInfo *AuthorizationInfo, opt ...Option) error {
|
|
|
|
|
// Ensure the object ID is set
|
|
|
|
|
if err := authzInfo.populateObjectId(ctx); err != nil {
|
2021-09-07 22:06:22 -04:00
|
|
|
return fmt.Errorf("error fetching object id: %w", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
opts, err := getOpts(opt...)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return fmt.Errorf("error parsing ops: %w", err)
|
|
|
|
|
}
|
|
|
|
|
|
2021-11-10 13:59:21 -05:00
|
|
|
secretId := authzInfo.AuthParams.SecretId
|
2021-09-07 22:06:22 -04:00
|
|
|
if opts.withSecretId != "" {
|
|
|
|
|
secretId = opts.withSecretId
|
|
|
|
|
}
|
|
|
|
|
|
2021-11-10 13:59:21 -05:00
|
|
|
aClient, err := getApplicationsClient(ctx, authzInfo)
|
2021-09-07 22:06:22 -04:00
|
|
|
if err != nil {
|
|
|
|
|
return fmt.Errorf("error getting application client: %w", err)
|
|
|
|
|
}
|
|
|
|
|
if aClient == nil {
|
|
|
|
|
return errors.New("applications client is nil when removing credential")
|
|
|
|
|
}
|
|
|
|
|
|
2021-11-10 13:59:21 -05:00
|
|
|
_, err = aClient.RemovePassword(ctx, authzInfo.AuthParams.ClientObjectId, secretId)
|
2021-09-07 22:06:22 -04:00
|
|
|
if err != nil {
|
|
|
|
|
return fmt.Errorf("error removing old password: %w", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return nil
|
|
|
|
|
}
|