Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 14 additions & 2 deletions assets/policyAssignment.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"fmt"
"unicode/utf8"

"github.com/Azure/alzlib/internal/parametername"
"github.com/Azure/alzlib/to"
"github.com/Azure/azure-sdk-for-go/sdk/azcore/arm"
"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armpolicy"
Expand Down Expand Up @@ -63,12 +64,22 @@ func (pa *PolicyAssignment) ReferencedPolicyDefinitionResourceIDAndVersion() (*a
}

// ParameterValueAsString returns the value of a policy assignment parameter.
// The parameter name is matched exactly first, then case-insensitively, as Azure Resource Manager
// treats parameter names as case-insensitive.
// We always expect the value to be a string as it's used in calculating the additional role
// assignments for
// policy parameters with the assignPermissions metadata set to true.
// Therefore the value should be an ARM resourceId.
func (pa *PolicyAssignment) ParameterValueAsString(paramName string) (string, error) {
paParamVal, ok := pa.Properties.Parameters[paramName]
match, ok, err := parametername.Resolve(pa.Properties.Parameters, paramName)
if err != nil {
return "", fmt.Errorf(
"PolicyAssignment.ParameterValueAsString: policy assignment %s: %w",
*pa.Name,
err,
)
}

if !ok {
return "", fmt.Errorf(
"PolicyAssignment.ParameterValueAsString: parameter %s not found in policy assignment %s",
Expand All @@ -77,7 +88,8 @@ func (pa *PolicyAssignment) ParameterValueAsString(paramName string) (string, er
)
}

if paParamVal.Value == nil {
paParamVal := match.Value
if paParamVal == nil || paParamVal.Value == nil {
return "", fmt.Errorf(
"PolicyAssignment.ParameterValueAsString: parameter %s value field in policy assignment %s is nil",
paramName,
Expand Down
62 changes: 38 additions & 24 deletions assets/policyDefinition.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"fmt"
"unicode/utf8"

"github.com/Azure/alzlib/internal/parametername"
"github.com/Azure/alzlib/to"
"github.com/Azure/azure-sdk-for-go/sdk/azcore/arm"
"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armpolicy"
Expand Down Expand Up @@ -242,15 +243,20 @@ func (pd *PolicyDefinition) AssignPermissionsParameterNames() ([]string, error)
}

// ParameterIsOptional checks if the parameter with the given name is optional in the policy definition.
// The name is matched case-insensitively if there is no exact match.
func (pd *PolicyDefinition) ParameterIsOptional(name string) (bool, error) {
if pd == nil || pd.Properties == nil || pd.Properties.Parameters == nil {
return false, errors.New(
"PolicyDefinition.ParameterIsOptional: policy definition is nil, missing properties or parameters",
)
}

param, ok := pd.Properties.Parameters[name]
if !ok {
_, param, found, err := pd.ResolveParameter(name)
if err != nil {
return false, err
}

if !found {
return false, fmt.Errorf(
"PolicyDefinition.ParameterIsOptional: parameter %s not found in policy definition",
name,
Expand All @@ -264,30 +270,45 @@ func (pd *PolicyDefinition) ParameterIsOptional(name string) (bool, error) {
return true, nil
}

// Parameter returns the parameter with the given name from the policy definition.
func (pd *PolicyDefinition) Parameter(name string) *armpolicy.ParameterDefinitionsValue {
// ResolveParameter returns the canonical name and value of the parameter matching the given name.
// Names are matched exactly first, then case-insensitively, as Azure Resource Manager treats
// parameter names as case-insensitive.
// The boolean return value reports whether a matching parameter was declared. An error is returned
// if the name matches more than one parameter when compared case-insensitively.
func (pd *PolicyDefinition) ResolveParameter(
name string,
) (string, *armpolicy.ParameterDefinitionsValue, bool, error) {
if pd == nil || pd.Properties == nil || pd.Properties.Parameters == nil {
return nil
return "", nil, false, nil
}

ret, ok := pd.Properties.Parameters[name]
if !ok {
match, found, err := parametername.Resolve(pd.Properties.Parameters, name)
if err != nil {
return "", nil, false, fmt.Errorf("PolicyDefinition.ResolveParameter: %w", err)
}

return match.Key, match.Value, found, nil
}

// Parameter returns the parameter with the given name from the policy definition.
// The name is matched case-insensitively if there is no exact match.
// It returns nil if the parameter is not found, or if the name is ambiguous.
func (pd *PolicyDefinition) Parameter(name string) *armpolicy.ParameterDefinitionsValue {
_, param, found, err := pd.ResolveParameter(name)
if err != nil || !found {
return nil
}

return ret
return param
}

// SetAssignPermissionsOnParameter sets the AssignPermissions metadata field to true for the
// parameter with the given
// name.
// The name is matched case-insensitively if there is no exact match.
func (pd *PolicyDefinition) SetAssignPermissionsOnParameter(parameterName string) {
if pd == nil || pd.Properties == nil || pd.Properties.Parameters == nil {
return
}

param, ok := pd.Properties.Parameters[parameterName]
if !ok {
param := pd.Parameter(parameterName)
if param == nil {
return
}

Expand All @@ -300,17 +321,10 @@ func (pd *PolicyDefinition) SetAssignPermissionsOnParameter(parameterName string

// UnsetAssignPermissionsOnParameter removes the AssignPermissions metadata field for the parameter
// with the given name.
// The name is matched case-insensitively if there is no exact match.
func (pd *PolicyDefinition) UnsetAssignPermissionsOnParameter(parameterName string) {
if pd == nil || pd.Properties == nil || pd.Properties.Parameters == nil {
return
}

param, ok := pd.Properties.Parameters[parameterName]
if !ok {
return
}

if param.Metadata == nil {
param := pd.Parameter(parameterName)
if param == nil || param.Metadata == nil {
return
}

Expand Down
94 changes: 94 additions & 0 deletions assets/policyDefinition_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,100 @@ func TestUnsetAssignPermissionsOnParameter(t *testing.T) {
assert.Nil(t, pd.Properties.Parameters["test"].Metadata.AssignPermissions)
}

// TestResolveParameterCaseInsensitive covers the built-in Windows AMA/DCR shape, where the
// initiative passes `DcrResourceId` to a definition declaring `dcrResourceId`.
func TestResolveParameterCaseInsensitive(t *testing.T) {
pd := &PolicyDefinition{
Definition: armpolicy.Definition{
Properties: &armpolicy.DefinitionProperties{
Parameters: map[string]*armpolicy.ParameterDefinitionsValue{
"dcrResourceId": {Type: to.Ptr(armpolicy.ParameterTypeString)},
},
},
},
}

name, param, found, err := pd.ResolveParameter("dcrResourceId")
require.NoError(t, err)
assert.True(t, found)
assert.Equal(t, "dcrResourceId", name)
assert.NotNil(t, param)

name, param, found, err = pd.ResolveParameter("DcrResourceId")
require.NoError(t, err)
assert.True(t, found)
assert.Equal(t, "dcrResourceId", name)
assert.NotNil(t, param)
assert.NotNil(t, pd.Parameter("DcrResourceId"))

_, _, found, err = pd.ResolveParameter("minPort")
require.NoError(t, err)
assert.False(t, found)
assert.Nil(t, pd.Parameter("minPort"))
}

func TestResolveParameterAmbiguous(t *testing.T) {
pd := &PolicyDefinition{
Definition: armpolicy.Definition{
Properties: &armpolicy.DefinitionProperties{
Parameters: map[string]*armpolicy.ParameterDefinitionsValue{
"dcrResourceId": {Type: to.Ptr(armpolicy.ParameterTypeString)},
"DCRRESOURCEID": {Type: to.Ptr(armpolicy.ParameterTypeString)},
},
},
},
}

_, _, found, err := pd.ResolveParameter("DcrResourceId")
require.Error(t, err)
assert.False(t, found)
require.ErrorContains(t, err, "is ambiguous")
assert.Nil(t, pd.Parameter("DcrResourceId"))
}

// TestAssignPermissionsAndOptionalParameterCaseInsensitive asserts that the parameter-name based
// helpers agree with Parameter() on casing, so a case-only variant does not silently no-op.
func TestAssignPermissionsAndOptionalParameterCaseInsensitive(t *testing.T) {
pd := &PolicyDefinition{
Definition: armpolicy.Definition{
Properties: &armpolicy.DefinitionProperties{
Parameters: map[string]*armpolicy.ParameterDefinitionsValue{
"dcrResourceId": {Type: to.Ptr(armpolicy.ParameterTypeString)},
},
},
},
}

optional, err := pd.ParameterIsOptional("DcrResourceId")
require.NoError(t, err)
assert.False(t, optional)

pd.SetAssignPermissionsOnParameter("DcrResourceId")
require.NotNil(t, pd.Properties.Parameters["dcrResourceId"].Metadata)
assert.True(t, *pd.Properties.Parameters["dcrResourceId"].Metadata.AssignPermissions)

pd.UnsetAssignPermissionsOnParameter("DCRRESOURCEID")
assert.Nil(t, pd.Properties.Parameters["dcrResourceId"].Metadata.AssignPermissions)

_, err = pd.ParameterIsOptional("minPort")
require.ErrorContains(t, err, "not found in policy definition")
}

func TestResolveParameterNilReceiverAndProperties(t *testing.T) {
var pd *PolicyDefinition

_, _, found, err := pd.ResolveParameter("dcrResourceId")
require.NoError(t, err)
assert.False(t, found)
assert.Nil(t, pd.Parameter("dcrResourceId"))

pd = &PolicyDefinition{Definition: armpolicy.Definition{}}

_, _, found, err = pd.ResolveParameter("dcrResourceId")
require.NoError(t, err)
assert.False(t, found)
}

func TestNewPolicyDefinitionFromVersionSuccess(t *testing.T) {
versionID := "/subscriptions/00000000-0000-0000-0000-000000000000/providers/" +
"Microsoft.Authorization/policyDefinitions/myPolicy/versions/1.0.0"
Expand Down
9 changes: 6 additions & 3 deletions assets/policySetDefinition.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"fmt"
"unicode/utf8"

"github.com/Azure/alzlib/internal/parametername"
"github.com/Azure/azure-sdk-for-go/sdk/azcore/arm"
"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armpolicy"
)
Expand Down Expand Up @@ -152,17 +153,19 @@ func (psd *PolicySetDefinition) PolicyDefinitionReferences() []*armpolicy.Defini
}

// Parameter returns the parameter definition for the given name.
// The name is matched case-insensitively if there is no exact match.
// It returns nil if the parameter is not found, or if the name is ambiguous.
func (psd *PolicySetDefinition) Parameter(name string) *armpolicy.ParameterDefinitionsValue {
if psd == nil || psd.Properties == nil || psd.Properties.Parameters == nil {
return nil
}

ret, ok := psd.Properties.Parameters[name]
if !ok {
match, found, err := parametername.Resolve(psd.Properties.Parameters, name)
if err != nil || !found {
return nil
}

return ret
return match.Value
}

// GetVersion returns the version of the policy definition, if it exists.
Expand Down
15 changes: 14 additions & 1 deletion deployment/hierarchy.go
Original file line number Diff line number Diff line change
Expand Up @@ -432,7 +432,20 @@ func (h *Hierarchy) addManagementGroup(
}

for param := range rf.Parameters {
if pd.Parameter(param) == nil {
_, _, found, err := pd.ResolveParameter(param)
if err != nil {
return nil, fmt.Errorf(
"Hierarchy.AddManagementGroup(): parameter `%s` in policy set definition `%s` "+
"is ambiguous in referenced definition `%s` in management group `%s`: %w",
param,
*psd.Name,
alzlib.JoinNameAndVersion(*pd.Name, rf.DefinitionVersion),
req.id,
err,
)
}

if !found {
return nil, fmt.Errorf(
"Hierarchy.AddManagementGroup(): parameter `%s` in policy set definition `%s` "+
"does not match a parameter in referenced definition `%s` in management group `%s`",
Expand Down
Loading
Loading