Skip to content

Commit

Permalink
Azure Secrets Engine Customizations (#174)
Browse files Browse the repository at this point in the history
* add azure customization fields

* delete json file i added by accident

* add changelog

* update description

* update with suggestions

* update with suggestions
  • Loading branch information
Zlaticanin authored Nov 22, 2023
1 parent f443395 commit b615e52
Show file tree
Hide file tree
Showing 8 changed files with 143 additions and 11 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ IMPROVEMENTS:
## v0.16.3

IMPROVEMENTS:
* Add sign_in_audience and tags fields to application registration [GH-174](https://github.com/hashicorp/vault-plugin-secrets-azure/pull/174)
* Prevent write-ahead-log data from being replicated to performance secondaries [GH-164](https://github.com/hashicorp/vault-plugin-secrets-azure/pull/164)
* Update dependencies [[GH-161]](https://github.com/hashicorp/vault-plugin-secrets-azure/pull/161)
* github.com/Azure/azure-sdk-for-go v68.0.0
Expand Down
10 changes: 8 additions & 2 deletions api/applications.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import (

type ApplicationsClient interface {
GetApplication(ctx context.Context, clientID string) (Application, error)
CreateApplication(ctx context.Context, displayName string) (Application, error)
CreateApplication(ctx context.Context, displayName string, signInAudience string, tags []string) (Application, error)
DeleteApplication(ctx context.Context, applicationObjectID string, permanentlyDelete bool) error
ListApplications(ctx context.Context, filter string) ([]Application, error)
AddApplicationPassword(ctx context.Context, applicationObjectID string, displayName string, endDateTime time.Time) (PasswordCredential, error)
Expand Down Expand Up @@ -121,9 +121,15 @@ func (c *MSGraphClient) ListApplications(ctx context.Context, filter string) ([]
}

// CreateApplication create a new Azure application object.
func (c *MSGraphClient) CreateApplication(ctx context.Context, displayName string) (Application, error) {
func (c *MSGraphClient) CreateApplication(ctx context.Context, displayName string, signInAudience string, tags []string) (Application, error) {
requestBody := models.NewApplication()
requestBody.SetDisplayName(&displayName)
requestBody.SetTags(tags)

// only set signInAudience if it's non-empty
if signInAudience != "" {
requestBody.SetSignInAudience(&signInAudience)
}

resp, err := c.client.Applications().Post(ctx, requestBody, nil)
if err != nil {
Expand Down
8 changes: 4 additions & 4 deletions client.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,22 +56,22 @@ func (c *client) Valid() bool {
// createApp creates a new Azure application.
// An Application is a needed to create service principals used by
// the caller for authentication.
func (c *client) createApp(ctx context.Context) (app api.Application, err error) {
func (c *client) createApp(ctx context.Context, signInAudience string, tags []string) (app api.Application, err error) {
// TODO: Make this name customizable with the same logic as username customization
name := uuid.New().String()

name = appNamePrefix + name

result, err := c.provider.CreateApplication(ctx, name)
result, err := c.provider.CreateApplication(ctx, name, signInAudience, tags)

return result, err
}

func (c *client) createAppWithName(ctx context.Context, rolename string) (app api.Application, err error) {
func (c *client) createAppWithName(ctx context.Context, rolename string, signInAudience string, tags []string) (app api.Application, err error) {
intSuffix := fmt.Sprintf("%d", time.Now().Unix())
name := fmt.Sprintf("%s%s-%s", appNamePrefix, rolename, intSuffix)

result, err := c.provider.CreateApplication(ctx, name)
result, err := c.provider.CreateApplication(ctx, name, signInAudience, tags)

return result, err
}
Expand Down
66 changes: 65 additions & 1 deletion path_roles.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (
"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/authorization/armauthorization/v2"
"github.com/hashicorp/vault/sdk/framework"
"github.com/hashicorp/vault/sdk/helper/jsonutil"
"github.com/hashicorp/vault/sdk/helper/strutil"
"github.com/hashicorp/vault/sdk/logical"

"github.com/hashicorp/vault-plugin-secrets-azure/api"
Expand All @@ -31,6 +32,8 @@ type roleEntry struct {
AzureGroups []*AzureGroup `json:"azure_groups"`
ApplicationID string `json:"application_id"`
ApplicationObjectID string `json:"application_object_id"`
SignInAudience string `json:"sign_in_audience"`
Tags []string `json:"tags"`
TTL time.Duration `json:"ttl"`
MaxTTL time.Duration `json:"max_ttl"`
PermanentlyDelete bool `json:"permanently_delete"`
Expand Down Expand Up @@ -87,6 +90,14 @@ func pathsRole(b *azureSecretBackend) []*framework.Path {
Type: framework.TypeString,
Description: "JSON list of Azure groups to add the service principal to.",
},
"sign_in_audience": {
Type: framework.TypeString,
Description: "Specifies the security principal types that are allowed to sign in to the application. Valid values are: AzureADMyOrg, AzureADMultipleOrgs, AzureADandPersonalMicrosoftAccount, PersonalMicrosoftAccount",
},
"tags": {
Type: framework.TypeCommaStringSlice,
Description: "Azure tags to attach to an application.",
},
"ttl": {
Type: framework.TypeDurationSecond,
Description: "Default lease for generated credentials. If not set or set to 0, will use system default.",
Expand Down Expand Up @@ -208,6 +219,32 @@ func (b *azureSecretBackend) pathRoleUpdate(ctx context.Context, req *logical.Re
role.PermanentlyDelete = false
}

// update and validate SignInAudience if provided
if signInAudience, ok := d.GetOk("sign_in_audience"); ok {
signInAudienceValue, ok := signInAudience.(string)
if !ok {
return logical.ErrorResponse("Invalid type for sign_in_audience field. Expected string."), nil
}

validSignInAudiences := []string{"AzureADMyOrg", "AzureADMultipleOrgs", "AzureADandPersonalMicrosoftAccount", "PersonalMicrosoftAccount"}
if !strutil.StrListContains(validSignInAudiences, signInAudienceValue) {
validValuesString := strings.Join(validSignInAudiences, ", ")
return logical.ErrorResponse("Invalid value for sign_in_audience field. Valid values are: %s", validValuesString), nil
}

role.SignInAudience = signInAudienceValue
}

// update and verify Tags if provided
tags, ok := d.GetOk("tags")
if ok {
tagsList, err := validateTags(tags)
if err != nil {
return logical.ErrorResponse(err.Error()), nil
}
role.Tags = tagsList
}

// update and verify Application Object ID if provided
if appObjectID, ok := d.GetOk("application_object_id"); ok {
role.ApplicationObjectID = appObjectID.(string)
Expand Down Expand Up @@ -354,6 +391,31 @@ func (b *azureSecretBackend) pathRoleUpdate(ctx context.Context, req *logical.Re
return resp, nil
}

func validateTags(tags interface{}) ([]string, error) {
if tags == nil {
return nil, nil
}

tagsList, ok := tags.([]string)
if !ok {
return nil, fmt.Errorf("expected tags to be []string, but got %T", tags)
}

tagsList = strutil.RemoveDuplicates(tagsList, false)

for _, tag := range tagsList {
// Check individual tag size
if len(tag) < 1 || len(tag) > 256 {
return nil, fmt.Errorf("individual tag size must be between 1 and 256 characters (inclusive)")
}
// Check for whitespaces
if strings.Contains(tag, " ") {
return nil, errors.New("whitespaces are not allowed in tags")
}
}
return tagsList, nil
}

func (b *azureSecretBackend) createPersistedApp(ctx context.Context, req *logical.Request, role *roleEntry, name string) error {

c, err := b.getClient(ctx, req.Storage)
Expand Down Expand Up @@ -387,7 +449,7 @@ func (b *azureSecretBackend) createPersistedApp(ctx context.Context, req *logica
return nil
}

app, err := c.createAppWithName(ctx, name)
app, err := c.createAppWithName(ctx, name, role.SignInAudience, role.Tags)
if err != nil {
return err
}
Expand Down Expand Up @@ -465,6 +527,8 @@ func (b *azureSecretBackend) pathRoleRead(ctx context.Context, req *logical.Requ
"application_object_id": r.ApplicationObjectID,
"permanently_delete": r.PermanentlyDelete,
"persist_app": r.PersistApp,
"sign_in_audience": r.SignInAudience,
"tags": r.Tags,
},
}
return resp, nil
Expand Down
61 changes: 61 additions & 0 deletions path_roles_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@ func TestRoleCreate(t *testing.T) {
"application_object_id": "",
"permanently_delete": true,
"persist_app": false,
"sign_in_audience": "AzureADMyOrg",
"tags": []string{"project:vault_test"},
}

spRole2 := map[string]interface{}{
Expand Down Expand Up @@ -72,6 +74,8 @@ func TestRoleCreate(t *testing.T) {
"application_object_id": "",
"permanently_delete": true,
"persist_app": false,
"sign_in_audience": "AzureADMultipleOrgs",
"tags": []string{"project:vault_test"},
}

// Verify basic updates of the name role
Expand Down Expand Up @@ -193,6 +197,8 @@ func TestRoleCreate(t *testing.T) {
"max_ttl": int64(3000),
"azure_roles": "[]",
"azure_groups": "[]",
"sign_in_audience": "PersonalMicrosoftAccount",
"tags": []string{"environment:production"},
"permanently_delete": false,
"persist_app": false,
}
Expand All @@ -217,6 +223,8 @@ func TestRoleCreate(t *testing.T) {
}]`,
),
"application_object_id": "",
"sign_in_audience": "AzureADandPersonalMicrosoftAccount",
"tags": []string{"project:vault_testing"},
"azure_groups": "[]",
"persist_app": false,
}
Expand Down Expand Up @@ -539,6 +547,59 @@ func TestRoleCreateBad(t *testing.T) {
if !strings.Contains(resp.Error().Error(), msg) {
t.Fatalf("expected to find: %s, got: %s", msg, resp.Error().Error())
}

// invalid signInAudience
role = map[string]interface{}{"sign_in_audience": "asdfg"}
resp = testRoleCreateBasic(t, b, s, "test_role_1", role)
msg = "Invalid value for sign_in_audience field. Valid values are: AzureADMyOrg, AzureADMultipleOrgs, AzureADandPersonalMicrosoftAccount, PersonalMicrosoftAccount"
if !strings.Contains(resp.Error().Error(), msg) {
t.Fatalf("expected to find: %s, got: %s", msg, resp.Error().Error())
}
}

func TestValidateTags(t *testing.T) {
tests := []struct {
name string
tags []string
valid bool
}{
{
name: "Valid tags",
tags: []string{"project:vault_test", "team:engineering"},
valid: true,
},
{
name: "Empty tags",
tags: []string{},
valid: true,
},
{
name: "Duplicate tags",
tags: []string{"project:vault_test", "project:vault_test"},
valid: true,
},
{
name: "Tags with whitespaces",
tags: []string{"environment development"},
valid: false,
},
{
name: "Invalid long tag size (must be between 1 and 256 characters)",
tags: []string{"abc" + strings.Repeat("d", 256)},
valid: false,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
_, err := validateTags(tt.tags)
if tt.valid && err != nil {
t.Errorf("unexpected error: %v", err)
} else if !tt.valid && err == nil {
t.Error("expected error but got nil")
}
})
}
}

func TestRoleUpdateError(t *testing.T) {
Expand Down
2 changes: 1 addition & 1 deletion path_service_principal.go
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ func (b *azureSecretBackend) createSPSecret(ctx context.Context, s logical.Stora
// Create the App, which is the top level object to be tracked in the secret
// and deleted upon revocation. If any subsequent step fails, the App will be
// deleted as part of WAL rollback.
app, err := c.createApp(ctx)
app, err := c.createApp(ctx, role.SignInAudience, role.Tags)
if err != nil {
return nil, err
}
Expand Down
4 changes: 2 additions & 2 deletions provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -159,8 +159,8 @@ func getClientOptions(s *clientSettings, httpClient *http.Client) *arm.ClientOpt
}

// CreateApplication create a new Azure application object.
func (p *provider) CreateApplication(ctx context.Context, displayName string) (result api.Application, err error) {
return p.appClient.CreateApplication(ctx, displayName)
func (p *provider) CreateApplication(ctx context.Context, displayName string, signInAudience string, tags []string) (result api.Application, err error) {
return p.appClient.CreateApplication(ctx, displayName, signInAudience, tags)
}

func (p *provider) GetApplication(ctx context.Context, applicationObjectID string) (result api.Application, err error) {
Expand Down
2 changes: 1 addition & 1 deletion provider_mock_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ func (m *mockProvider) CreateServicePrincipal(_ context.Context, _ string, _ tim
return id, pass, nil
}

func (m *mockProvider) CreateApplication(_ context.Context, _ string) (api.Application, error) {
func (m *mockProvider) CreateApplication(_ context.Context, _ string, _ string, _ []string) (api.Application, error) {
if m.ctxTimeout != 0 {
// simulate a context deadline error by sleeping for timeout period
time.Sleep(m.ctxTimeout)
Expand Down

0 comments on commit b615e52

Please sign in to comment.