Skip to content

Commit

Permalink
azure: Limit VMSS scope to specific storage account
Browse files Browse the repository at this point in the history
  • Loading branch information
hakman committed May 13, 2024
1 parent 1e1bc0b commit 017545b
Show file tree
Hide file tree
Showing 8 changed files with 163 additions and 49 deletions.
37 changes: 18 additions & 19 deletions pkg/model/azuremodel/vmscaleset.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,16 +65,26 @@ func (b *VMScaleSetModelBuilder) Build(c *fi.CloudupModelBuilderContext) error {
if ig.IsControlPlane() || b.Cluster.UsesLegacyGossip() {
// Create tasks for assigning built-in roles to VM Scale Sets.
// See https://docs.microsoft.com/en-us/azure/role-based-access-control/built-in-roles
// for the ID definitions.
roleDefIDs := map[string]string{
resourceGroupID := fmt.Sprintf("/subscriptions/%s/resourceGroups/%s",
b.Cluster.Spec.CloudProvider.Azure.SubscriptionID,
b.Cluster.Spec.CloudProvider.Azure.ResourceGroupName,
)
c.AddTask(&azuretasks.RoleAssignment{
Name: to.Ptr(fmt.Sprintf("%s-%s", *vmss.Name, "owner")),
Lifecycle: b.Lifecycle,
Scope: to.Ptr(resourceGroupID),
VMScaleSet: vmss,
// Owner
"owner": "8e3af657-a8ff-443c-a75c-2fe8c4bcb635",
RoleDefID: to.Ptr("8e3af657-a8ff-443c-a75c-2fe8c4bcb635"),
})
c.AddTask(&azuretasks.RoleAssignment{
Name: to.Ptr(fmt.Sprintf("%s-%s", *vmss.Name, "blob")),
Lifecycle: b.Lifecycle,
Scope: to.Ptr(b.Cluster.Spec.CloudProvider.Azure.StorageAccountID),
VMScaleSet: vmss,
// Storage Blob Data Contributor
"blob": "ba92f5b4-2d11-453d-a403-e96b0029c9fe",
}
for k, roleDefID := range roleDefIDs {
c.AddTask(b.buildRoleAssignmentTask(vmss, k, roleDefID))
}
RoleDefID: to.Ptr("ba92f5b4-2d11-453d-a403-e96b0029c9fe"),
})
}
}

Expand Down Expand Up @@ -247,14 +257,3 @@ func parseImage(image string) (*compute.ImageReference, error) {
Version: to.Ptr(l[3]),
}, nil
}

func (b *VMScaleSetModelBuilder) buildRoleAssignmentTask(vmss *azuretasks.VMScaleSet, roleKey, roleDefID string) *azuretasks.RoleAssignment {
name := fmt.Sprintf("%s-%s", *vmss.Name, roleKey)
return &azuretasks.RoleAssignment{
Name: to.Ptr(name),
Lifecycle: b.Lifecycle,
ResourceGroup: b.LinkToResourceGroup(),
VMScaleSet: vmss,
RoleDefID: to.Ptr(roleDefID),
}
}
21 changes: 20 additions & 1 deletion upup/pkg/fi/cloudup/azure/azure_cloud.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import (
"strings"

"github.com/Azure/azure-sdk-for-go/sdk/azidentity"
"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage"
"k8s.io/klog/v2"
"k8s.io/kops/dnsprovider/pkg/dnsprovider"
"k8s.io/kops/pkg/apis/kops"
Expand All @@ -45,6 +46,7 @@ type AzureCloud interface {
fi.Cloud
AddClusterTags(tags map[string]*string)
FindVNetInfo(id, resourceGroup string) (*fi.VPCInfo, error)
FindStorageAccountInfo(name string) (*armstorage.Account, error)
SubscriptionID() string
ResourceGroup() ResourceGroupsClient
VirtualNetwork() VirtualNetworksClient
Expand Down Expand Up @@ -81,6 +83,7 @@ type azureCloudImplementation struct {
loadBalancersClient LoadBalancersClient
publicIPAddressesClient PublicIPAddressesClient
natGatewaysClient NatGatewaysClient
storageAccountsClient StorageAccountsClient
}

var _ fi.Cloud = &azureCloudImplementation{}
Expand Down Expand Up @@ -141,6 +144,9 @@ func NewAzureCloud(subscriptionID, resourceGroupName, location string, tags map[
if azureCloudImpl.natGatewaysClient, err = newNatGatewaysClientImpl(subscriptionID, cred); err != nil {
return nil, err
}
if azureCloudImpl.storageAccountsClient, err = newStorageAccountsClientImpl(subscriptionID, cred); err != nil {
return nil, err
}

return azureCloudImpl, nil
}
Expand All @@ -158,7 +164,7 @@ func (c *azureCloudImplementation) DNS() (dnsprovider.Interface, error) {
}

func (c *azureCloudImplementation) FindVPCInfo(id string) (*fi.VPCInfo, error) {
return nil, errors.New("FindVPCInfo not implemented on azureCloud, use FindVNETInfo instead")
return nil, errors.New("FindVPCInfo not implemented on azureCloud, use FindVNetInfo instead")
}

func (c *azureCloudImplementation) FindVNetInfo(id, resourceGroup string) (*fi.VPCInfo, error) {
Expand Down Expand Up @@ -194,6 +200,19 @@ func (c *azureCloudImplementation) FindVNetInfo(id, resourceGroup string) (*fi.V
return nil, nil
}

func (c *azureCloudImplementation) FindStorageAccountInfo(name string) (*armstorage.Account, error) {
sas, err := c.storageAccountsClient.List(context.TODO())
if err != nil {
return nil, err
}
for _, sa := range sas {
if *sa.Name == name {
return sa, nil
}
}
return nil, nil
}

func (c *azureCloudImplementation) DeleteInstance(i *cloudinstances.CloudInstance) error {
vmssName := i.CloudInstanceGroup.HumanName
instanceID := strings.TrimPrefix(i.ID, vmssName+"_")
Expand Down
6 changes: 3 additions & 3 deletions upup/pkg/fi/cloudup/azure/roleassignment.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ import (
// RoleAssignmentsClient is a client for managing role assignments
type RoleAssignmentsClient interface {
Create(ctx context.Context, scope, roleAssignmentName string, parameters authz.RoleAssignmentCreateParameters) (*authz.RoleAssignment, error)
List(ctx context.Context, resourceGroupName string) ([]*authz.RoleAssignment, error)
List(ctx context.Context, scope string) ([]*authz.RoleAssignment, error)
Delete(ctx context.Context, scope, raName string) error
}

Expand All @@ -47,9 +47,9 @@ func (c *roleAssignmentsClientImpl) Create(
return &resp.RoleAssignment, err
}

func (c *roleAssignmentsClientImpl) List(ctx context.Context, resourceGroupName string) ([]*authz.RoleAssignment, error) {
func (c *roleAssignmentsClientImpl) List(ctx context.Context, scope string) ([]*authz.RoleAssignment, error) {
var l []*authz.RoleAssignment
pager := c.c.NewListForResourceGroupPager(resourceGroupName, nil)
pager := c.c.NewListForScopePager(scope, nil)
for pager.More() {
resp, err := pager.NextPage(ctx)
if err != nil {
Expand Down
59 changes: 59 additions & 0 deletions upup/pkg/fi/cloudup/azure/storageaccount.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
/*
Copyright 2024 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package azure

import (
"context"
"fmt"

"github.com/Azure/azure-sdk-for-go/sdk/azidentity"
"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage"
)

// StorageAccountsClient is a client for managing Network Interfaces.
type StorageAccountsClient interface {
List(ctx context.Context) ([]*armstorage.Account, error)
}

type storageAccountsClientImpl struct {
c *armstorage.AccountsClient
}

var _ StorageAccountsClient = &storageAccountsClientImpl{}

func (c *storageAccountsClientImpl) List(ctx context.Context) ([]*armstorage.Account, error) {
var l []*armstorage.Account
pager := c.c.NewListPager(nil)
for pager.More() {
resp, err := pager.NextPage(ctx)
if err != nil {
return nil, fmt.Errorf("listing storage accounts: %w", err)
}
l = append(l, resp.Value...)
}
return l, nil
}

func newStorageAccountsClientImpl(subscriptionID string, cred *azidentity.DefaultAzureCredential) (*storageAccountsClientImpl, error) {
c, err := armstorage.NewAccountsClient(subscriptionID, cred, nil)
if err != nil {
return nil, fmt.Errorf("creating storage accounts client: %w", err)
}
return &storageAccountsClientImpl{
c: c,
}, nil
}
20 changes: 8 additions & 12 deletions upup/pkg/fi/cloudup/azuretasks/roleassignment.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,10 +43,10 @@ type RoleAssignment struct {
Name *string
Lifecycle fi.Lifecycle

ResourceGroup *ResourceGroup
VMScaleSet *VMScaleSet
ID *string
RoleDefID *string
Scope *string
VMScaleSet *VMScaleSet
ID *string
RoleDefID *string
}

var (
Expand All @@ -69,7 +69,7 @@ func (r *RoleAssignment) Find(c *fi.CloudupContext) (*RoleAssignment, error) {
}

cloud := c.T.Cloud.(azure.AzureCloud)
rs, err := cloud.RoleAssignment().List(context.TODO(), *r.ResourceGroup.Name)
rs, err := cloud.RoleAssignment().List(context.TODO(), *r.Scope)
if err != nil {
return nil, err
}
Expand All @@ -95,7 +95,7 @@ func (r *RoleAssignment) Find(c *fi.CloudupContext) (*RoleAssignment, error) {
}

// Query VM Scale Sets and find one that has matching Principal ID.
vs, err := cloud.VMScaleSet().List(context.TODO(), *r.ResourceGroup.Name)
vs, err := cloud.VMScaleSet().List(context.TODO(), *r.VMScaleSet.ResourceGroup.Name)
if err != nil {
return nil, err
}
Expand All @@ -117,9 +117,7 @@ func (r *RoleAssignment) Find(c *fi.CloudupContext) (*RoleAssignment, error) {
return &RoleAssignment{
Name: r.Name,
Lifecycle: r.Lifecycle,
ResourceGroup: &ResourceGroup{
Name: r.ResourceGroup.Name,
},
Scope: found.Properties.Scope,
VMScaleSet: &VMScaleSet{
Name: foundVMSS.Name,
},
Expand Down Expand Up @@ -165,9 +163,7 @@ func createNewRoleAssignment(t *azure.AzureAPITarget, e *RoleAssignment) error {
// We generate the name of Role Assignment here. It must be a valid GUID.
roleAssignmentName := uuid.New().String()

// TODO(kenji): Append additinoal scope ("providers/Microsoft.Storage/storageAccounts/<account-name>") when the role is for blob access so that
// the role is scoped to a specific storage account.
scope := fmt.Sprintf("/subscriptions/%s/resourceGroups/%s", t.Cloud.SubscriptionID(), *e.ResourceGroup.Name)
scope := *e.Scope
roleDefID := fmt.Sprintf("%s/providers/Microsoft.Authorization/roleDefinitions/%s", scope, *e.RoleDefID)
roleAssignment := authz.RoleAssignmentCreateParameters{
Properties: &authz.RoleAssignmentProperties{
Expand Down
26 changes: 13 additions & 13 deletions upup/pkg/fi/cloudup/azuretasks/roleassignment_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,8 @@ func TestRoleAssignmentRenderAzure(t *testing.T) {
apiTarget := azure.NewAzureAPITarget(cloud)
ra := &RoleAssignment{}
expected := &RoleAssignment{
Name: to.Ptr("ra"),
ResourceGroup: &ResourceGroup{
Name: to.Ptr("rg"),
},
Name: to.Ptr("ra"),
Scope: to.Ptr("scope"),
VMScaleSet: &VMScaleSet{
Name: to.Ptr("vmss"),
PrincipalID: to.Ptr("pid"),
Expand Down Expand Up @@ -75,16 +73,18 @@ func TestRoleAssignmentFind(t *testing.T) {
t.Fatalf("failed to create: %s", err)
}
vmss := &VMScaleSet{
Name: to.Ptr(vmssName),
PrincipalID: resp.Identity.PrincipalID,
Name: to.Ptr(vmssName),
PrincipalID: resp.Identity.PrincipalID,
ResourceGroup: rg,
}

scope := "scope"
roleDefID := "rdid0"
ra := &RoleAssignment{
Name: vmss.Name,
ResourceGroup: rg,
VMScaleSet: vmss,
RoleDefID: &roleDefID,
Name: vmss.Name,
Scope: &scope,
VMScaleSet: vmss,
RoleDefID: &roleDefID,
}
// Find will return nothing if there is no Role Assignment created.
actual, err := ra.Find(ctx)
Expand All @@ -97,7 +97,6 @@ func TestRoleAssignmentFind(t *testing.T) {

// Create Role Assignments. One of them has irrelevant (different role definition ID).
roleAssignmentName := uuid.New().String()
scope := "s"
roleAssignment := authz.RoleAssignmentCreateParameters{
Properties: &authz.RoleAssignmentProperties{
RoleDefinitionID: to.Ptr(roleDefID),
Expand Down Expand Up @@ -163,9 +162,10 @@ func TestRoleAssignmentFind_NoPrincipalID(t *testing.T) {
t.Fatalf("failed to create Role Assignment: %s", err)
}

scope := "scope"
ra := &RoleAssignment{
Name: to.Ptr(vmssName),
ResourceGroup: rg,
Name: to.Ptr(vmssName),
Scope: to.Ptr(scope),
VMScaleSet: &VMScaleSet{
Name: to.Ptr(vmssName),
// Do not set principal ID.
Expand Down
29 changes: 28 additions & 1 deletion upup/pkg/fi/cloudup/azuretasks/testing.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import (
compute "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute"
network "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork"
resources "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources"
"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage"
"github.com/google/uuid"
v1 "k8s.io/api/core/v1"
"k8s.io/kops/dnsprovider/pkg/dnsprovider"
Expand Down Expand Up @@ -56,6 +57,7 @@ type MockAzureCloud struct {
LoadBalancersClient *MockLoadBalancersClient
PublicIPAddressesClient *MockPublicIPAddressesClient
NatGatewaysClient *MockNatGatewaysClient
StorageAccountsClient *MockStorageAccountsClient
}

var _ azure.AzureCloud = &MockAzureCloud{}
Expand Down Expand Up @@ -106,6 +108,9 @@ func NewMockAzureCloud(location string) *MockAzureCloud {
NatGatewaysClient: &MockNatGatewaysClient{
NGWs: map[string]*network.NatGateway{},
},
StorageAccountsClient: &MockStorageAccountsClient{
SAs: map[string]*armstorage.Account{},
},
}
}

Expand All @@ -129,10 +134,16 @@ func (c *MockAzureCloud) FindVPCInfo(id string) (*fi.VPCInfo, error) {
return nil, errors.New("FindVPCInfo not implemented on azureCloud")
}

// FindVNetInfo returns the VPCInfo.
func (c *MockAzureCloud) FindVNetInfo(id, resourceGroup string) (*fi.VPCInfo, error) {
return nil, errors.New("FindVNetInfo not implemented on azureCloud")
}

// FindStorageAccountInfo returns the storage account info.
func (c *MockAzureCloud) FindStorageAccountInfo(name string) (*armstorage.Account, error) {
return nil, errors.New("FindStorageAccountInfo not implemented on azureCloud")
}

// DeleteInstance deletes the instance.
func (c *MockAzureCloud) DeleteInstance(i *cloudinstances.CloudInstance) error {
return errors.New("DeleteInstance not implemented on azureCloud")
Expand Down Expand Up @@ -539,7 +550,7 @@ func (c *MockRoleAssignmentsClient) Create(
}

// List returns a slice of role assignments.
func (c *MockRoleAssignmentsClient) List(ctx context.Context, resourceGroupName string) ([]*authz.RoleAssignment, error) {
func (c *MockRoleAssignmentsClient) List(ctx context.Context, scope string) ([]*authz.RoleAssignment, error) {
var l []*authz.RoleAssignment
for _, ra := range c.RAs {
l = append(l, ra)
Expand Down Expand Up @@ -798,3 +809,19 @@ func (c *MockNatGatewaysClient) Delete(ctx context.Context, resourceGroupName, n
delete(c.NGWs, ngwName)
return nil
}

// MockStorageAccountsClient is a mock implementation of Nat Gateway client.
type MockStorageAccountsClient struct {
SAs map[string]*armstorage.Account
}

var _ azure.StorageAccountsClient = &MockStorageAccountsClient{}

// List returns a slice of Storage Accounts.
func (c *MockStorageAccountsClient) List(ctx context.Context) ([]*armstorage.Account, error) {
var l []*armstorage.Account
for _, sa := range c.SAs {
l = append(l, sa)
}
return l, nil
}
Loading

0 comments on commit 017545b

Please sign in to comment.