deployment/hierarchy.go (284 lines of code) (raw):

// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. package deployment import ( "context" "errors" "fmt" "slices" "strings" "sync" "github.com/Azure/alzlib" "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" mapset "github.com/deckarep/golang-set/v2" "github.com/google/uuid" ) const ( ManagementGroupIdFmt = "/providers/Microsoft.Management/managementGroups/%s" PolicyAssignmentIdFmt = "/providers/Microsoft.Management/managementGroups/%s/providers/Microsoft.Authorization/policyAssignments/%s" PolicyDefinitionIdFmt = "/providers/Microsoft.Management/managementGroups/%s/providers/Microsoft.Authorization/policyDefinitions/%s" PolicySetDefinitionIdFmt = "/providers/Microsoft.Management/managementGroups/%s/providers/Microsoft.Authorization/policySetDefinitions/%s" RoleDefinitionIdFmt = "/providers/Microsoft.Management/managementGroups/%s/providers/Microsoft.Authorization/roleDefinitions/%s" ) // Hierarchy represents a deployment of Azure management group hierarchy. // Do not create this struct directly, use NewHierarchy instead. type Hierarchy struct { mgs map[string]*HierarchyManagementGroup alzlib *alzlib.AlzLib mu *sync.RWMutex } // NewHierarchy creates a new Hierarchy with the given AlzLib. func NewHierarchy(alzlib *alzlib.AlzLib) *Hierarchy { return &Hierarchy{ mgs: make(map[string]*HierarchyManagementGroup), alzlib: alzlib, mu: new(sync.RWMutex), } } // ManagementGroup returns the management group with the given name. func (h *Hierarchy) ManagementGroup(name string) *HierarchyManagementGroup { h.mu.RLock() defer h.mu.RUnlock() if mg, ok := h.mgs[name]; ok { return mg } return nil } // ManagementGroupNames returns the management group names as a slice of string. func (h *Hierarchy) ManagementGroupNames() []string { h.mu.RLock() defer h.mu.RUnlock() res := make([]string, len(h.mgs)) i := 0 for mgname := range h.mgs { res[i] = mgname i++ } slices.Sort(res) return res } // ManagementGroups returns the management groups from the given level as a map of string to *HierarchyManagementGroup. func (h *Hierarchy) ManagementGroupsAtLevel(level int) map[string]*HierarchyManagementGroup { h.mu.RLock() defer h.mu.RUnlock() res := make(map[string]*HierarchyManagementGroup) for mgname, mg := range h.mgs { if mg.level != level { continue } res[mgname] = mg } return h.mgs } // FromArchitecture creates a hierarchy from the given architecture. func (h *Hierarchy) FromArchitecture(ctx context.Context, arch, externalParentId, location string) error { architecture := h.alzlib.Architecture(arch) if architecture == nil { return fmt.Errorf("Hierarchy.FromArchitecture: error getting architecture `%s`", arch) } // Get the architecture root management groups. for _, a := range architecture.RootMgs() { if err := recurseAddManagementGroup(ctx, h, a, externalParentId, location, true, 0); err != nil { return fmt.Errorf("Hierarchy.FromArchitecture: recursion error on architecture `%s` %w", arch, err) } } return nil } // PolicyAssignments returns the policy assignments required for the hierarchy. // This error returned bay be a PolicyAssignmentErrors, which contains a slice of errors. // This is so that callers can choose to issue a warning here instead of halting the process. func (h *Hierarchy) PolicyRoleAssignments(ctx context.Context) (mapset.Set[PolicyRoleAssignment], error) { h.mu.RLock() defer h.mu.RUnlock() var errs *PolicyRoleAssignmentErrors res := mapset.NewThreadUnsafeSet[PolicyRoleAssignment]() // Get the policy assignments for each management group. for _, mg := range h.mgs { if err := mg.generatePolicyAssignmentAdditionalRoleAssignments(); err != nil { var thisErrs *PolicyRoleAssignmentErrors if errors.As(err, &thisErrs) { if errs == nil { errs = NewPolicyRoleAssignmentErrors() } errs.Add(thisErrs.Errors()...) continue } return nil, fmt.Errorf("Hierarchy.PolicyRoleAssignments: error generating additional role assignments for management group `%s`: %w", mg.id, err) } res = res.Union(mg.policyRoleAssignments) } if errs != nil { return res, errs } return res, nil } // AddDefaultPolicyAssignmentValue adds a default policy assignment value to the hierarchy. func (h *Hierarchy) AddDefaultPolicyAssignmentValue(ctx context.Context, defaultName string, defaultValue *armpolicy.ParameterValuesValue) error { defs := h.alzlib.PolicyDefaultValue(defaultName) if defs == nil { return fmt.Errorf("Hierarchy.AddDefaultPolicyAssignmentValue: A default with name `%s` does not exist", defaultName) } // Get the policy assignments for each management group. for _, mg := range h.mgs { for assignment, params := range defs.PolicyAssignment2ParameterMap() { if _, ok := mg.policyAssignments[assignment]; !ok { continue } newParams := make(map[string]*armpolicy.ParameterValuesValue) for param := range params.Iter() { newParams[param] = defaultValue } if err := mg.ModifyPolicyAssignment(assignment, newParams, nil, nil, nil, nil, nil); err != nil { return fmt.Errorf("Hierarchy.AddDefaultPolicyAssignmentValue: error adding default `%s` policy assignment value to management group `%s` for policy assignment `%s`: %w", defaultName, mg.id, assignment, err) } } } return nil } func recurseAddManagementGroup(ctx context.Context, h *Hierarchy, archMg *alzlib.ArchitectureManagementGroup, parent, location string, externalParent bool, level int) error { req := managementGroupAddRequest{ archetypes: archMg.Archetypes(), displayName: archMg.DisplayName(), exists: archMg.Exists(), id: archMg.Id(), level: level, location: location, parentId: parent, parentIsExternal: externalParent, } if _, err := h.addManagementGroup(ctx, req); err != nil { return fmt.Errorf("Hierarchy.recurseAddManagementGroup: error adding management group `%s`: %w", archMg.Id(), err) } for _, child := range archMg.Children() { if err := recurseAddManagementGroup(ctx, h, child, archMg.Id(), location, false, level+1); err != nil { return err } } return nil } // addManagementGroup adds a management group to the hierarchy, with a parent if specified. // If the parent is not specified, the management group is considered the root of the hierarchy. // The archetype should have been obtained using the `AlzLib.CopyArchetype` method. // This allows for customization and ensures the correct policy assignment values have been set. func (h *Hierarchy) addManagementGroup(ctx context.Context, req managementGroupAddRequest) (*HierarchyManagementGroup, error) { if req.parentId == "" { return nil, fmt.Errorf("Hierarchy.AddManagementGroup: parent management group not specified for `%s`", req.id) } h.mu.Lock() defer h.mu.Unlock() if _, exists := h.mgs[req.id]; exists { return nil, fmt.Errorf("Hierarchy.AddManagementGroup: management group %s already exists", req.id) } mg := newManagementGroup() mg.id = req.id mg.displayName = req.displayName mg.exists = req.exists mg.level = req.level mg.children = mapset.NewSet[*HierarchyManagementGroup]() mg.location = req.location if req.parentIsExternal { if _, ok := h.mgs[req.parentId]; ok { return nil, fmt.Errorf("Hierarchy.AddManagementGroup: external parent management group set, but already exists %s", req.parentId) } mg.parentExternal = to.Ptr(req.parentId) } if !req.parentIsExternal { parentMg, ok := h.mgs[req.parentId] if !ok { return nil, fmt.Errorf("Hierarchy.AddManagementGroup: parent management group not found %s", req.parentId) } mg.parent = parentMg h.mgs[req.parentId].children.Add(mg) } // Get the policy definitions and policy set definitions referenced by the policy assignments. assignedPolicyDefinitionIds := mapset.NewThreadUnsafeSet[string]() // Combine all assignments form all supplied archetypes into a single set allPolicyAssignments := mapset.NewThreadUnsafeSet[string]() for _, archetype := range req.archetypes { allPolicyAssignments = allPolicyAssignments.Union(archetype.PolicyAssignments) } for pa := range allPolicyAssignments.Iter() { polAssign := h.alzlib.PolicyAssignment(pa) if polAssign == nil { return nil, fmt.Errorf("Hierarchy.AddManagementGroup: policy assignment `%s` referenced in management group `%s` does not exist in the library", pa, req.id) } referencedResourceId, err := polAssign.ReferencedPolicyDefinitionResourceId() if err != nil { return nil, fmt.Errorf("Hierarchy.AddManagementGroup: error getting referenced policy definition resource ID for policy assignment `%s` in management group `%s`: %w", pa, req.id, err) } assignedPolicyDefinitionIds.Add(referencedResourceId.String()) } if err := h.alzlib.GetDefinitionsFromAzure(ctx, assignedPolicyDefinitionIds.ToSlice()); err != nil { return nil, fmt.Errorf("Hierarchy.AddManagementGroup: adding mg `%s` error getting policy definitions from Azure: %w", req.id, err) } // Now that we are sure that we have all the definitions in the library, // make copies of the archetype resources for modification in the Deployment management group. // Copmbine all policy definitions form all supplied archetypes into a single set allPolicyDefinitions := mapset.NewThreadUnsafeSet[string]() for _, archetype := range req.archetypes { allPolicyDefinitions = allPolicyDefinitions.Union(archetype.PolicyDefinitions) } for name := range allPolicyDefinitions.Iter() { newDef := h.alzlib.PolicyDefinition(name) if newDef == nil { return nil, fmt.Errorf("Hierarchy.AddManagementGroup: policy definition `%s` in management group `%s` does not exist in the library", name, req.id) } mg.policyDefinitions[name] = newDef } // Combine all policy set definitions form all supplied archetypes into a single set allPolicySetDefinitions := mapset.NewThreadUnsafeSet[string]() for _, archetype := range req.archetypes { allPolicySetDefinitions = allPolicySetDefinitions.Union(archetype.PolicySetDefinitions) } for name := range allPolicySetDefinitions.Iter() { newSetDef := h.alzlib.PolicySetDefinition(name) if newSetDef == nil { return nil, fmt.Errorf("Hierarchy.AddManagementGroup(): policy set definition `%s` in management group `%s` does not exist in the library", name, req.id) } mg.policySetDefinitions[name] = newSetDef } // Now that the policy definitions and policy set definitions have been copied, we can add the policy assignments for name := range allPolicyAssignments.Iter() { newpolassign := h.alzlib.PolicyAssignment(name) if newpolassign == nil { return nil, fmt.Errorf("Hierarchy.AddManagementGroup(): policy assignment `%s` in management group `%s` does not exist in the library", name, req.id) } // Check if the referenced policy is a set and if its parameters match the parameters in the policy definitions refPdId, _ := newpolassign.ReferencedPolicyDefinitionResourceId() if refPdId.ResourceType.Type == "policySetDefinitions" { psd := h.alzlib.PolicySetDefinition(refPdId.Name) rfs := psd.PolicyDefinitionReferences() for _, rf := range rfs { resId, _ := arm.ParseResourceID(*rf.PolicyDefinitionID) pd := h.alzlib.PolicyDefinition(resId.Name) if pd == nil { return nil, fmt.Errorf("Hierarchy.AddManagementGroup(): policy definition `%s` in policy set definition `%s` in management group `%s` does not exist in the library", resId.Name, refPdId.Name, req.id) } for param := range rf.Parameters { if pd.Parameter(param) == nil { 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`", param, *psd.Name, *pd.Name, req.id) } } } } mg.policyAssignments[name] = newpolassign } // Combine all role definitions form all supplied archetypes into a single set allRoleDefinitions := mapset.NewThreadUnsafeSet[string]() for _, archetype := range req.archetypes { allRoleDefinitions = allRoleDefinitions.Union(archetype.RoleDefinitions) } for name := range allRoleDefinitions.Iter() { newroledef := h.alzlib.RoleDefinition(name) if newroledef == nil { return nil, fmt.Errorf("Hierarchy.AddManagementGroup(): role definition `%s` in management group `%s` does not exist in the library", name, req.id) } mg.roleDefinitions[name] = newroledef } // set the hierarchy on the management group. mg.hierarchy = h // add the management group to the deployment. h.mgs[req.id] = mg // run Update to change all refs, etc. if err := h.mgs[req.id].update(); err != nil { return nil, fmt.Errorf("Hierarchy.AddManagementGroup: adding `%s` error updating assets at scope %w", req.id, err) } return mg, nil } // policyDefinitionToMg returns a map of policy definition names to the deployed management group name. func (d *Hierarchy) policyDefinitionToMg() map[string]mapset.Set[string] { res := make(map[string]mapset.Set[string], 0) for mgname, mg := range d.mgs { for pdname := range mg.policyDefinitions { if _, ok := res[pdname]; !ok { res[pdname] = mapset.NewThreadUnsafeSet[string]() } res[pdname].Add(mgname) } } return res } // policyDefinitionToMg returns a map of policy set definition names to the deployed management group name. func (d *Hierarchy) policySetDefinitionToMg() map[string]mapset.Set[string] { res := make(map[string]mapset.Set[string], 0) for mgname, mg := range d.mgs { for psdname := range mg.policySetDefinitions { if _, ok := res[psdname]; !ok { res[psdname] = mapset.NewThreadUnsafeSet[string]() } res[psdname].Add(mgname) } } return res } func uuidV5(s ...string) uuid.UUID { return uuid.NewSHA1(uuid.NameSpaceURL, []byte(strings.Join(s, ""))) }