Skip to content

Commit

Permalink
Add AccessControl subpackage in Azure VPC code
Browse files Browse the repository at this point in the history
This change refactors the code and adds a separate package in
Azure package accesscontrol for better control of Networking
Security Group Rules in Azure.

This code additionally implements App Connectivity in Azure based
on labels.
  • Loading branch information
Ignacy Osetek committed May 23, 2024
1 parent 69f7fcc commit ddd6afc
Show file tree
Hide file tree
Showing 18 changed files with 1,732 additions and 847 deletions.
226 changes: 137 additions & 89 deletions azure/accessControl.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,74 +20,14 @@ package azure
import (
"context"
"fmt"
"strings"

"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork"
accesscontrol "github.com/app-net-interface/awi-infra-guard/azure/accessControl"
"github.com/app-net-interface/awi-infra-guard/connector/helper"
"github.com/app-net-interface/awi-infra-guard/grpc/go/infrapb"
"github.com/app-net-interface/awi-infra-guard/types"
)

type vpcPolicy string

const (
vpcPolicyAllow = "allow"
vpcPolicyDeny = "deny"
)

// func (c *Client) refreshVnetSubnetsWithVPCPolicy(
// ctx context.Context,
// vnet armnetwork.VirtualNetwork,
// inboundVnet string,
// policy vpcPolicy,
// ) error {
// c.logger.Trace(
// "updating virtual network '%s' subnets with VPC Policy %s",
// vnet,
// )
// if vnet.Properties == nil {
// c.logger.Warnf(
// "virtual network '%s' has no properties - skipping policy update",
// helper.StringPointerToString(vnet.ID),
// )
// return nil
// }

// for i := range vnet.Properties.Subnets {
// if vnet.Properties.Subnets[i] == nil {
// c.logger.Warnf(
// "virtual network '%s' has a nil subnet pointer - skipping subnet entry",
// helper.StringPointerToString(vnet.ID),
// )
// continue
// }
// if vnet.Properties.Subnets[i].Properties == nil {
// c.logger.Warnf(
// "virtual network '%s' has a subnet %s with no properties - skipping subnet entry",
// helper.StringPointerToString(vnet.ID),
// helper.StringPointerToString(vnet.Properties.Subnets[i].ID),
// )
// continue
// }
// }

// return nil
// }

func getVnetSourceIDFromAWITags(tags map[string]string) (string, error) {
tagValue, ok := tags["awi"]
if !ok {
return "", fmt.Errorf(
"expected request key tag 'awi' with source ID but found none. Got tags: %v",
tags,
)
}
if !strings.HasPrefix(tagValue, "default-") {
return "", fmt.Errorf(
"the value of 'awi' tag from request has invalid prefix. Expected 'default-' but got: %s",
tagValue,
)
}
return strings.TrimPrefix(tagValue, "default-"), nil
}

// AccessControl interface implementation
func (c *Client) AddInboundAllowRuleInVPC(
ctx context.Context,
Expand All @@ -98,7 +38,6 @@ func (c *Client) AddInboundAllowRuleInVPC(
ruleName string,
tags map[string]string,
) error {

vnet, vnetAccount, err := c.getVPC(
ctx, destinationVpcID, region,
)
Expand All @@ -113,38 +52,150 @@ func (c *Client) AddInboundAllowRuleInVPC(
account = vnetAccount
}

sourceID, err := getVnetSourceIDFromAWITags(tags)
if err != nil {
return fmt.Errorf(
"failed to obtain the ID of Source VPC: %w", err,
)
}
ruleset := accesscontrol.AccessControlRuleSet{}
ruleset.NewDirectedVPCRules(
accesscontrol.CustomRuleName(ruleName),
accesscontrol.AccessAllow,
cidrsToAllow,
)

err = c.refreshSubnetSecurityGroupWithVPCInbound(
err = c.ApplyAccessRulesToVPC(
ctx,
account,
region,
cidrsToAllow,
vpcPolicyAllow,
vnet,
sourceID,
ruleName,
ruleset,
)
if err != nil {
return fmt.Errorf(
"failed to refresh Security Groups for subnets from VNet %s: %w",
destinationVpcID, err,
"failed to apply rules %v to VPC %s: %w",
ruleset, destinationVpcID, err,
)
}

return nil
}

func (c *Client) getSubnetsFromInstances(
ctx context.Context, instances []types.Instance,
) ([]armnetwork.Subnet, error) {

type subnetInfo struct {
VNetID string
SubnetID string
}

subnetInfos := helper.Set[subnetInfo]{}

for _, instance := range instances {
subnetInfos.Set(subnetInfo{
VNetID: instance.VPCID,
SubnetID: instance.SubnetID,
})
}

infos := subnetInfos.Keys()
subnets := make([]armnetwork.Subnet, 0, len(infos))

for _, info := range infos {
subnet, _, err := c.getSubnet(
ctx,
parseResourceGroupName(info.SubnetID),
parseResourceName(info.VNetID),
parseResourceName(info.SubnetID),
)
if err != nil {
return nil, fmt.Errorf(
"failed to get subnet %s: %w",
info.SubnetID, err,
)
}
subnets = append(subnets, subnet)
}

return subnets, nil
}

func (c *Client) prepareCustomAccessRules(
instances []types.Instance,
ruleName string,
cidrsToAllow []string,
protocolsAndPorts types.ProtocolsAndPorts,
) (accesscontrol.AccessControlRuleSet, error) {
ruleset := accesscontrol.AccessControlRuleSet{}

for _, instance := range instances {
err := ruleset.NewCustomRules(
accesscontrol.CustomRuleName(ruleName),
accesscontrol.AccessAllow,
[]string{instance.SubnetID},
cidrsToAllow,
[]string{instance.PrivateIP},
protocolsAndPorts,
)
if err != nil {
return accesscontrol.AccessControlRuleSet{}, fmt.Errorf(
"failed to create custom rule: %w", err,
)
}
}

return ruleset, nil
}

func (c *Client) AddInboundAllowRuleByLabelsMatch(ctx context.Context, account, region string,
vpcID string, ruleName string, labels map[string]string, cidrsToAllow []string,
protocolsAndPorts types.ProtocolsAndPorts) (ruleId string, instances []types.Instance, err error) {
// TBD
return "", nil, nil

instances, err = c.ListInstances(ctx, &infrapb.ListInstancesRequest{
VpcId: vpcID,
Zone: region,
AccountId: account,
Labels: labels,
Region: region,
})
if err != nil {
return "", nil, fmt.Errorf(
"failed to list Instances: %w", err,
)
}

subnets, err := c.getSubnetsFromInstances(ctx, instances)
if err != nil {
return "", nil, fmt.Errorf(
"failed to extract subnets associated with matched instances: %w", err,
)
}

ruleset, err := c.prepareCustomAccessRules(
instances,
ruleName,
cidrsToAllow,
protocolsAndPorts,
)
if err != nil {
return "", nil, fmt.Errorf(
"failed to prepare custom access rules: %w", err,
)
}

for _, subnet := range subnets {
err = c.ApplyAccessRulesToSubnet(
ctx,
account,
region,
subnet,
ruleset,
)
if err != nil {
return "", nil, fmt.Errorf(
"failed to apply access rules to subnet %s: %w",
helper.StringPointerToString(subnet.ID),
err,
)
}
}

return ruleName, instances, nil
}

func (c *Client) AddInboundAllowRuleBySubnetMatch(ctx context.Context, account, region string,
Expand All @@ -168,7 +219,7 @@ func (c *Client) AddInboundAllowRuleForLoadBalancerByDNS(ctx context.Context, ac
}

func (c *Client) RemoveInboundAllowRuleFromVPCByName(ctx context.Context, account, region string, vpcID string, ruleName string) error {
vnet, vnetAccount, err := c.getVPC(
vnet, _, err := c.getVPC(
ctx, vpcID, region,
)
if err != nil {
Expand All @@ -177,16 +228,13 @@ func (c *Client) RemoveInboundAllowRuleFromVPCByName(ctx context.Context, accoun
vpcID, err,
)
}
if account == "" {
account = vnetAccount
}

err = c.deleteVPCInboundFromSubnets(
err = c.DeleteAccessRulesFromVPC(
ctx,
account,
region,
vnet,
ruleName,
accesscontrol.RuleNames{
accesscontrol.CustomRuleName(ruleName),
},
)
if err != nil {
return fmt.Errorf(
Expand Down
74 changes: 74 additions & 0 deletions azure/accessControl/name.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
package accesscontrol

import (
"crypto/sha256"
"fmt"
"slices"
"strings"
)

// ruleName is unexported string created to
// enforce using exported functions from the
// package when setting up names for Access
// Control resources outside of this package.
type ruleName string

// RuleNames is an exported slice of ruleName used
// mainly to specify rules that should be removed.
type RuleNames = []ruleName

// VPCRuleName generates proper name identifier
// based on source and destination VPCs. The VPC
// rule acts bidirectional and so the order of
// VPC names will be picked by the function
// (names are sorted to keep it deterministic).
//
// TODO: Currently the name is a hash of vpc IDs,
// to keep the length of generated name fixed and
// not over accepted Azure limits, however it is
// not collision-proof. Name collision must be
// handled properly.
func VPCRuleName(vpcId1, vpcId2 string) ruleName {
ids := []string{vpcId1, vpcId2}
slices.Sort(ids)

hasher := sha256.New()
hasher.Write([]byte(strings.Join(ids, ":")))
hashBytes := hasher.Sum(nil)

return ruleName(fmt.Sprintf("%x", hashBytes))
}

// CustomRuleName accepts a regular name provided
// by the external entity and hashes it to keep
// the length name consistent.
//
// TODO: Currently the name is a hash of a given
// string, to keep the length of generated nam
// fixed and not over accepted Azure limits,
// however it is not collision-proof. Name
// collision must be handled properly.
func CustomRuleName(name string) ruleName {
hasher := sha256.New()
hasher.Write([]byte(name))
hashBytes := hasher.Sum(nil)

return ruleName(fmt.Sprintf("%x", hashBytes))
}

// nameWithPriority combines Rule name with fixed-length priority string.
// The priority always uses ":" character and 4 digits. For priorities
// lower than 1000, the actual priority is preceeded with 0s to match
// 4 characters length.
//
// The priority acts as a name distinguisher between rules inside the
// same Network Security Group as the name prefix may be equal but
// priority ensures uniqueness.
func nameWithPriority(name ruleName, priority uint) (string, error) {
if priority >= 10000 {
return "", fmt.Errorf(
"unexpected priority value - expected 4 digits at max: %d", priority,
)
}
return string(name) + fmt.Sprintf(":%04d", priority), nil
}
Loading

0 comments on commit ddd6afc

Please sign in to comment.