Skip to content
Merged
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
14 changes: 14 additions & 0 deletions server/azure/bastion/bastion_ops.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,15 @@ package bastion

import (
"net/http"
"strings"

"github.com/stackshy/cloudemu/v2/server/wire/azurearm"
bastiondriver "github.com/stackshy/cloudemu/v2/services/bastion/driver"
)

// bastionSubnetName is the only subnet name ARM accepts for a Bastion host.
const bastionSubnetName = "AzureBastionSubnet"

// createOrUpdateBastionHost handles PUT .../bastionHosts/{name}. The whole host
// arrives in one body and fully REPLACES the stored state, matching ARM's
// CreateOrUpdate semantics. BastionHosts.CreateOrUpdate is an LRO; returning the
Expand All @@ -20,6 +24,16 @@ func (h *Handler) createOrUpdateBastionHost(w http.ResponseWriter, r *http.Reque

host := buildBastionHost(&body)

for i := range host.IPConfigurations {
subnet := host.IPConfigurations[i].SubnetID
if subnet != "" && subnet[strings.LastIndex(subnet, "/")+1:] != bastionSubnetName {
azurearm.WriteError(w, http.StatusBadRequest, "InvalidParameter",
"Subnet "+subnet+" used by Bastion host must be named "+bastionSubnetName+".")

return
}
}

stored, created, err := h.hosts.CreateOrUpdateBastionHost(r.Context(), rp.ResourceGroup, rp.ResourceName, host)
if err != nil {
azurearm.WriteCErr(w, err)
Expand Down
37 changes: 37 additions & 0 deletions server/azure/bastion/subnet_name_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package bastion_test

import (
"context"
"errors"
"strings"
"testing"

"github.com/Azure/azure-sdk-for-go/sdk/azcore"
"github.com/Azure/azure-sdk-for-go/sdk/azcore/to"
)

// TestBastionRejectsWrongSubnetName proves a Bastion host whose ipConfiguration
// subnet is not named AzureBastionSubnet is refused with a 400.
func TestBastionRejectsWrongSubnetName(t *testing.T) {
ts := newServer(t)
c := bastionClient(t, ts)

body := sampleHost()
body.Properties.IPConfigurations[0].Properties.Subnet.ID = to.Ptr(
"/subscriptions/sub-1/resourceGroups/rg-1/providers/Microsoft.Network/virtualNetworks/vnet/subnets/default")

_, err := c.BeginCreateOrUpdate(context.Background(), testRG, hostName, body, nil)

var respErr *azcore.ResponseError
if !errors.As(err, &respErr) {
t.Fatalf("want ResponseError, got %v", err)
}

if respErr.StatusCode != 400 || respErr.ErrorCode != "InvalidParameter" {
t.Errorf("got %d %s, want 400 InvalidParameter", respErr.StatusCode, respErr.ErrorCode)
}

if !strings.Contains(respErr.Error(), "AzureBastionSubnet") {
t.Errorf("message should mention AzureBastionSubnet: %v", respErr)
}
}
8 changes: 5 additions & 3 deletions server/azure/vnet/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -1709,16 +1709,18 @@ func (h *Handler) deletePublicIP(w http.ResponseWriter, r *http.Request, rp azur

if h.publicIPConfigurationRef(r.Context(), rp.Subscription, id) != nil {
azurearm.WriteError(w, http.StatusBadRequest, "PublicIPAddressCannotBeDeleted",
"public IP "+rp.ResourceName+" is still referenced by a network interface ipConfiguration")
"Public IP address "+id+" can not be deleted since it is still allocated to a network interface ipConfiguration.")

return
}

if err := h.net.ReleaseAddress(r.Context(), info.AllocationID); err != nil {
// A public IP still bound to a NIC/NAT gateway: ARM answers 400 with
// this specific code, not the generic 409 WriteCErr would emit.
// this specific code, naming the ARM resource id (never the internal
// allocation id), not the generic 409 WriteCErr would emit.
if cerrors.IsFailedPrecondition(err) {
azurearm.WriteError(w, http.StatusBadRequest, "PublicIPAddressCannotBeDeleted", cerrors.Message(err))
azurearm.WriteError(w, http.StatusBadRequest, "PublicIPAddressCannotBeDeleted",
"Public IP address "+id+" can not be deleted since it is still allocated to a resource.")
return
}

Expand Down
74 changes: 74 additions & 0 deletions server/azure/vnet/public_ip_delete_message_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
package vnet_test

import (
"context"
"errors"
"strings"
"testing"

"github.com/Azure/azure-sdk-for-go/sdk/azcore"
"github.com/Azure/azure-sdk-for-go/sdk/azcore/to"
"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork"
)

// TestSDKPublicIPDeleteInUseNamesResource checks the error for deleting a
// NAT-gateway-bound public IP is a 400 PublicIPAddressCannotBeDeleted naming the ARM
// resource id, not the internal allocation id.
func TestSDKPublicIPDeleteInUseNamesResource(t *testing.T) {
ts := newVNetServer(t)
ctx := context.Background()
opts := clientOpts(ts)

pips, err := armnetwork.NewPublicIPAddressesClient("sub-1", fakeCred{}, opts)
if err != nil {
t.Fatal(err)
}

pp, err := pips.BeginCreateOrUpdate(ctx, "rg-1", "pip-busy", armnetwork.PublicIPAddress{
Location: to.Ptr("eastus"),
SKU: &armnetwork.PublicIPAddressSKU{Name: to.Ptr(armnetwork.PublicIPAddressSKUNameStandard)},
Properties: &armnetwork.PublicIPAddressPropertiesFormat{
PublicIPAllocationMethod: to.Ptr(armnetwork.IPAllocationMethodStatic),
},
}, nil)
if err != nil {
t.Fatalf("create pip: %v", err)
}

pollDone(t, pp)

pipID := "/subscriptions/sub-1/resourceGroups/rg-1/providers/Microsoft.Network/publicIPAddresses/pip-busy"

nats, err := armnetwork.NewNatGatewaysClient("sub-1", fakeCred{}, opts)
if err != nil {
t.Fatal(err)
}

np, err := nats.BeginCreateOrUpdate(ctx, "rg-1", "natgw-busy", armnetwork.NatGateway{
Location: to.Ptr("eastus"),
SKU: &armnetwork.NatGatewaySKU{Name: to.Ptr(armnetwork.NatGatewaySKUNameStandard)},
Properties: &armnetwork.NatGatewayPropertiesFormat{
PublicIPAddresses: []*armnetwork.SubResource{{ID: to.Ptr(pipID)}},
},
}, nil)
if err != nil {
t.Fatalf("create nat: %v", err)
}

pollDone(t, np)

_, err = pips.BeginDelete(ctx, "rg-1", "pip-busy", nil)

var respErr *azcore.ResponseError
if !errors.As(err, &respErr) {
t.Fatalf("want ResponseError, got %v", err)
}

if respErr.StatusCode != 400 || respErr.ErrorCode != "PublicIPAddressCannotBeDeleted" {
t.Errorf("got %d %s, want 400 PublicIPAddressCannotBeDeleted", respErr.StatusCode, respErr.ErrorCode)
}

if strings.Contains(respErr.Error(), "ipalloc-") || !strings.Contains(respErr.Error(), pipID) {
t.Errorf("message should name %s and not an internal id: %v", pipID, respErr)
}
}
Loading