Skip to content

Commit 1881abd

Browse files
author
Ignacy Osetek
committed
Add AccessControl subpackage in Azure VPC code
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.
1 parent 69f7fcc commit 1881abd

18 files changed

+1649
-845
lines changed

azure/accessControl.go

Lines changed: 137 additions & 89 deletions
Original file line numberDiff line numberDiff line change
@@ -20,74 +20,14 @@ package azure
2020
import (
2121
"context"
2222
"fmt"
23-
"strings"
2423

24+
"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork"
25+
accesscontrol "github.com/app-net-interface/awi-infra-guard/azure/accessControl"
26+
"github.com/app-net-interface/awi-infra-guard/connector/helper"
27+
"github.com/app-net-interface/awi-infra-guard/grpc/go/infrapb"
2528
"github.com/app-net-interface/awi-infra-guard/types"
2629
)
2730

28-
type vpcPolicy string
29-
30-
const (
31-
vpcPolicyAllow = "allow"
32-
vpcPolicyDeny = "deny"
33-
)
34-
35-
// func (c *Client) refreshVnetSubnetsWithVPCPolicy(
36-
// ctx context.Context,
37-
// vnet armnetwork.VirtualNetwork,
38-
// inboundVnet string,
39-
// policy vpcPolicy,
40-
// ) error {
41-
// c.logger.Trace(
42-
// "updating virtual network '%s' subnets with VPC Policy %s",
43-
// vnet,
44-
// )
45-
// if vnet.Properties == nil {
46-
// c.logger.Warnf(
47-
// "virtual network '%s' has no properties - skipping policy update",
48-
// helper.StringPointerToString(vnet.ID),
49-
// )
50-
// return nil
51-
// }
52-
53-
// for i := range vnet.Properties.Subnets {
54-
// if vnet.Properties.Subnets[i] == nil {
55-
// c.logger.Warnf(
56-
// "virtual network '%s' has a nil subnet pointer - skipping subnet entry",
57-
// helper.StringPointerToString(vnet.ID),
58-
// )
59-
// continue
60-
// }
61-
// if vnet.Properties.Subnets[i].Properties == nil {
62-
// c.logger.Warnf(
63-
// "virtual network '%s' has a subnet %s with no properties - skipping subnet entry",
64-
// helper.StringPointerToString(vnet.ID),
65-
// helper.StringPointerToString(vnet.Properties.Subnets[i].ID),
66-
// )
67-
// continue
68-
// }
69-
// }
70-
71-
// return nil
72-
// }
73-
74-
func getVnetSourceIDFromAWITags(tags map[string]string) (string, error) {
75-
tagValue, ok := tags["awi"]
76-
if !ok {
77-
return "", fmt.Errorf(
78-
"expected request key tag 'awi' with source ID but found none. Got tags: %v",
79-
tags,
80-
)
81-
}
82-
if !strings.HasPrefix(tagValue, "default-") {
83-
return "", fmt.Errorf(
84-
"the value of 'awi' tag from request has invalid prefix. Expected 'default-' but got: %s",
85-
tagValue,
86-
)
87-
}
88-
return strings.TrimPrefix(tagValue, "default-"), nil
89-
}
90-
9131
// AccessControl interface implementation
9232
func (c *Client) AddInboundAllowRuleInVPC(
9333
ctx context.Context,
@@ -98,7 +38,6 @@ func (c *Client) AddInboundAllowRuleInVPC(
9838
ruleName string,
9939
tags map[string]string,
10040
) error {
101-
10241
vnet, vnetAccount, err := c.getVPC(
10342
ctx, destinationVpcID, region,
10443
)
@@ -113,38 +52,150 @@ func (c *Client) AddInboundAllowRuleInVPC(
11352
account = vnetAccount
11453
}
11554

116-
sourceID, err := getVnetSourceIDFromAWITags(tags)
117-
if err != nil {
118-
return fmt.Errorf(
119-
"failed to obtain the ID of Source VPC: %w", err,
120-
)
121-
}
55+
ruleset := accesscontrol.AccessControlRuleSet{}
56+
ruleset.NewDirectedVPCRules(
57+
accesscontrol.CustomRuleName(ruleName),
58+
accesscontrol.AccessAllow,
59+
cidrsToAllow,
60+
)
12261

123-
err = c.refreshSubnetSecurityGroupWithVPCInbound(
62+
err = c.ApplyAccessRulesToVPC(
12463
ctx,
12564
account,
126-
region,
127-
cidrsToAllow,
128-
vpcPolicyAllow,
12965
vnet,
130-
sourceID,
131-
ruleName,
66+
ruleset,
13267
)
13368
if err != nil {
13469
return fmt.Errorf(
135-
"failed to refresh Security Groups for subnets from VNet %s: %w",
136-
destinationVpcID, err,
70+
"failed to apply rules %v to VPC %s: %w",
71+
ruleset, destinationVpcID, err,
13772
)
13873
}
13974

14075
return nil
14176
}
14277

78+
func (c *Client) getSubnetsFromInstances(
79+
ctx context.Context, instances []types.Instance,
80+
) ([]armnetwork.Subnet, error) {
81+
82+
type subnetInfo struct {
83+
VNetID string
84+
SubnetID string
85+
}
86+
87+
subnetInfos := helper.Set[subnetInfo]{}
88+
89+
for _, instance := range instances {
90+
subnetInfos.Set(subnetInfo{
91+
VNetID: instance.VPCID,
92+
SubnetID: instance.SubnetID,
93+
})
94+
}
95+
96+
infos := subnetInfos.Keys()
97+
subnets := make([]armnetwork.Subnet, 0, len(infos))
98+
99+
for _, info := range infos {
100+
subnet, _, err := c.getSubnet(
101+
ctx,
102+
parseResourceGroupName(info.SubnetID),
103+
parseResourceName(info.VNetID),
104+
parseResourceName(info.SubnetID),
105+
)
106+
if err != nil {
107+
return nil, fmt.Errorf(
108+
"failed to get subnet %s: %w",
109+
info.SubnetID, err,
110+
)
111+
}
112+
subnets = append(subnets, subnet)
113+
}
114+
115+
return subnets, nil
116+
}
117+
118+
func (c *Client) prepareCustomAccessRules(
119+
instances []types.Instance,
120+
ruleName string,
121+
cidrsToAllow []string,
122+
protocolsAndPorts types.ProtocolsAndPorts,
123+
) (accesscontrol.AccessControlRuleSet, error) {
124+
ruleset := accesscontrol.AccessControlRuleSet{}
125+
126+
for _, instance := range instances {
127+
err := ruleset.NewCustomRules(
128+
accesscontrol.CustomRuleName(ruleName),
129+
accesscontrol.AccessAllow,
130+
[]string{instance.SubnetID},
131+
cidrsToAllow,
132+
[]string{instance.PrivateIP},
133+
protocolsAndPorts,
134+
)
135+
if err != nil {
136+
return accesscontrol.AccessControlRuleSet{}, fmt.Errorf(
137+
"failed to create custom rule: %w", err,
138+
)
139+
}
140+
}
141+
142+
return ruleset, nil
143+
}
144+
143145
func (c *Client) AddInboundAllowRuleByLabelsMatch(ctx context.Context, account, region string,
144146
vpcID string, ruleName string, labels map[string]string, cidrsToAllow []string,
145147
protocolsAndPorts types.ProtocolsAndPorts) (ruleId string, instances []types.Instance, err error) {
146-
// TBD
147-
return "", nil, nil
148+
149+
instances, err = c.ListInstances(ctx, &infrapb.ListInstancesRequest{
150+
VpcId: vpcID,
151+
Zone: region,
152+
AccountId: account,
153+
Labels: labels,
154+
Region: region,
155+
})
156+
if err != nil {
157+
return "", nil, fmt.Errorf(
158+
"failed to list Instances: %w", err,
159+
)
160+
}
161+
162+
subnets, err := c.getSubnetsFromInstances(ctx, instances)
163+
if err != nil {
164+
return "", nil, fmt.Errorf(
165+
"failed to extract subnets associated with matched instances: %w", err,
166+
)
167+
}
168+
169+
ruleset, err := c.prepareCustomAccessRules(
170+
instances,
171+
ruleName,
172+
cidrsToAllow,
173+
protocolsAndPorts,
174+
)
175+
if err != nil {
176+
return "", nil, fmt.Errorf(
177+
"failed to prepare custom access rules: %w", err,
178+
)
179+
}
180+
181+
for _, subnet := range subnets {
182+
err = c.ApplyAccessRulesToSubnet(
183+
ctx,
184+
account,
185+
region,
186+
subnet,
187+
ruleset,
188+
)
189+
if err != nil {
190+
return "", nil, fmt.Errorf(
191+
"failed to apply access rules to subnet %s: %w",
192+
helper.StringPointerToString(subnet.ID),
193+
err,
194+
)
195+
}
196+
}
197+
198+
return ruleName, instances, nil
148199
}
149200

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

170221
func (c *Client) RemoveInboundAllowRuleFromVPCByName(ctx context.Context, account, region string, vpcID string, ruleName string) error {
171-
vnet, vnetAccount, err := c.getVPC(
222+
vnet, _, err := c.getVPC(
172223
ctx, vpcID, region,
173224
)
174225
if err != nil {
@@ -177,16 +228,13 @@ func (c *Client) RemoveInboundAllowRuleFromVPCByName(ctx context.Context, accoun
177228
vpcID, err,
178229
)
179230
}
180-
if account == "" {
181-
account = vnetAccount
182-
}
183231

184-
err = c.deleteVPCInboundFromSubnets(
232+
err = c.DeleteAccessRulesFromVPC(
185233
ctx,
186-
account,
187-
region,
188234
vnet,
189-
ruleName,
235+
accesscontrol.RuleNames{
236+
accesscontrol.CustomRuleName(ruleName),
237+
},
190238
)
191239
if err != nil {
192240
return fmt.Errorf(

azure/accessControl/name.go

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
package accesscontrol
2+
3+
import (
4+
"crypto/sha256"
5+
"fmt"
6+
"slices"
7+
"strings"
8+
)
9+
10+
// ruleName is unexported string created to
11+
// enforce using exported functions from the
12+
// package when setting up names for Access
13+
// Control resources outside of this package.
14+
type ruleName string
15+
16+
// RuleNames is an exported slice of ruleName used
17+
// mainly to specify rules that should be removed.
18+
type RuleNames = []ruleName
19+
20+
// VPCRuleName generates proper name identifier
21+
// based on source and destination VPCs. The VPC
22+
// rule acts bidirectional and so the order of
23+
// VPC names will be picked by the function
24+
// (names are sorted to keep it deterministic).
25+
//
26+
// TODO: Currently the name is a hash of vpc IDs,
27+
// to keep the length of generated name fixed and
28+
// not over accepted Azure limits, however it is
29+
// not collision-proof. Name collision must be
30+
// handled properly.
31+
func VPCRuleName(vpcId1, vpcId2 string) ruleName {
32+
ids := []string{vpcId1, vpcId2}
33+
slices.Sort(ids)
34+
35+
hasher := sha256.New()
36+
hasher.Write([]byte(strings.Join(ids, ":")))
37+
hashBytes := hasher.Sum(nil)
38+
39+
return ruleName(fmt.Sprintf("%x", hashBytes))
40+
}
41+
42+
// CustomRuleName accepts a regular name provided
43+
// by the external entity and hashes it to keep
44+
// the length name consistent.
45+
//
46+
// TODO: Currently the name is a hash of a given
47+
// string, to keep the length of generated nam
48+
// fixed and not over accepted Azure limits,
49+
// however it is not collision-proof. Name
50+
// collision must be handled properly.
51+
func CustomRuleName(name string) ruleName {
52+
hasher := sha256.New()
53+
hasher.Write([]byte(name))
54+
hashBytes := hasher.Sum(nil)
55+
56+
return ruleName(fmt.Sprintf("%x", hashBytes))
57+
}
58+
59+
// nameWithPriority combines Rule name with fixed-length priority string.
60+
// The priority always uses ":" character and 4 digits. For priorities
61+
// lower than 1000, the actual priority is preceeded with 0s to match
62+
// 4 characters length.
63+
//
64+
// The priority acts as a name distinguisher between rules inside the
65+
// same Network Security Group as the name prefix may be equal but
66+
// priority ensures uniqueness.
67+
func nameWithPriority(name ruleName, priority uint) (string, error) {
68+
if priority >= 10000 {
69+
return "", fmt.Errorf(
70+
"unexpected priority value - expected 4 digits at max: %d", priority,
71+
)
72+
}
73+
return string(name) + fmt.Sprintf(":%04d", priority), nil
74+
}

0 commit comments

Comments
 (0)