pkg/validate/dynamic/dynamic.go (929 lines of code) (raw):

package dynamic // Copyright (c) Microsoft Corporation. // Licensed under the Apache License 2.0. import ( "context" "fmt" "net" "net/http" "strings" "time" "github.com/Azure/azure-sdk-for-go/sdk/azcore" "github.com/Azure/azure-sdk-for-go/sdk/azcore/policy" sdknetwork "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v2" "github.com/Azure/checkaccess-v2-go-sdk/client" "github.com/Azure/go-autorest/autorest" "github.com/Azure/go-autorest/autorest/azure" "github.com/apparentlymart/go-cidr/cidr" "github.com/sirupsen/logrus" "k8s.io/apimachinery/pkg/util/wait" "github.com/Azure/ARO-RP/pkg/api" apisubnet "github.com/Azure/ARO-RP/pkg/api/util/subnet" "github.com/Azure/ARO-RP/pkg/env" "github.com/Azure/ARO-RP/pkg/util/azureclient" "github.com/Azure/ARO-RP/pkg/util/azureclient/azuresdk/armauthorization" "github.com/Azure/ARO-RP/pkg/util/azureclient/azuresdk/armmsi" "github.com/Azure/ARO-RP/pkg/util/azureclient/azuresdk/armnetwork" "github.com/Azure/ARO-RP/pkg/util/azureclient/mgmt/compute" "github.com/Azure/ARO-RP/pkg/util/azureclient/mgmt/network" "github.com/Azure/ARO-RP/pkg/util/stringutils" "github.com/Azure/ARO-RP/pkg/util/token" ) var ( errMsgNSGAttached = "The provided subnet '%s' is invalid: must not have a network security group attached." errMsgOriginalNSGNotAttached = "The provided subnet '%s' is invalid: must have network security group '%s' attached." errMsgNSGNotAttached = "The provided subnet '%s' is invalid: must have a network security group attached." errMsgNSGNotProperlyAttached = "When the enable-preconfigured-nsg option is specified, both the master and worker subnets should have network security groups (NSG) attached to them before starting the cluster installation." errMsgSPHasNoRequiredPermissionsOnNSG = "The %s service principal (Application ID: %s) does not have Network Contributor role on network security group '%s'. This is required when the enable-preconfigured-nsg option is specified." errMsgWIHasNoRequiredPermissionsOnNSG = "The %s platform managed identity does not have required permissions on network security group '%s'. This is required when the enable-preconfigured-nsg option is specified." errMsgSubnetNotFound = "The provided subnet '%s' could not be found." errMsgSPHasNoRequiredPermissionsOnSubnet = "The %s service principal (Application ID: %s) does not have Network Contributor role on subnet '%s'." errMsgWIHasNoRequiredPermissionsOnSubnet = "The %s platform managed identity does not have required permissions on subnet '%s'." errMsgSubnetNotInSucceededState = "The provided subnet '%s' is not in a Succeeded state" errMsgSubnetInvalidSize = "The provided subnet '%s' is invalid: must be /27 or larger." errMsgSPHasNoRequiredPermissionsOnVNet = "The %s service principal (Application ID: %s) does not have Network Contributor role on vnet '%s'." errMsgWIHasNoRequiredPermissionsOnVNet = "The %s platform managed identity does not have required permissions on vnet '%s'." errMsgVnetNotFound = "The vnet '%s' could not be found." errMsgSPHasNoRequiredPermissionsOnRT = "The %s service principal does not have Network Contributor role on route table '%s'." errMsgWIHasNoRequiredPermissionsOnRT = "The %s platform managed identity does not have required permissions on route table '%s'." errMsgRTNotFound = "The route table '%s' could not be found." errMsgSPHasNoRequiredPermissionsOnNatGW = "The %s service principal does not have Network Contributor role on nat gateway '%s'." errMsgWIHasNoRequiredPermissionsOnNatGW = "The %s platform managed identity does not have required permissions on nat gateway '%s'." errMsgNatGWNotFound = "The nat gateway '%s' could not be found." errMsgCIDROverlaps = "The provided CIDRs must not overlap: '%s'." errMsgInvalidVNetLocation = "The vnet location '%s' must match the cluster location '%s'." ) const minimumSubnetMaskSize int = 27 type Subnet struct { // ID is a resource id of the subnet ID string // Path is a path in the cluster document. For example, properties.workerProfiles[0].subnetId Path string } type ServicePrincipalValidator interface { ValidateServicePrincipal(ctx context.Context, spTokenCredential azcore.TokenCredential) error } // Dynamic validate in the operator context. type Dynamic interface { ServicePrincipalValidator ValidateVnet(ctx context.Context, location string, subnets []Subnet, additionalCIDRs ...string) error ValidateSubnets(ctx context.Context, oc *api.OpenShiftCluster, subnets []Subnet) error ValidateDiskEncryptionSets(ctx context.Context, oc *api.OpenShiftCluster) error ValidateLoadBalancerProfile(ctx context.Context, oc *api.OpenShiftCluster) error ValidatePreConfiguredNSGs(ctx context.Context, oc *api.OpenShiftCluster, subnets []Subnet) error ValidateClusterUserAssignedIdentity(ctx context.Context, platformIdentities map[string]api.PlatformWorkloadIdentity, roleDefinitions armauthorization.RoleDefinitionsClient) error ValidatePlatformWorkloadIdentityProfile( ctx context.Context, oc *api.OpenShiftCluster, platformWorkloadIdentityRolesByRoleName map[string]api.PlatformWorkloadIdentityRole, roleDefinitions armauthorization.RoleDefinitionsClient, clusterMsiFederatedIdentityCredentials armmsi.FederatedIdentityCredentialsClient, platformWorkloadIdentities map[string]api.PlatformWorkloadIdentity, ) error } type dynamic struct { log *logrus.Entry appID *string // for use when reporting an error authorizerType AuthorizerType // This represents the Subject for CheckAccess. Could be either FP or SP. checkAccessSubjectInfoCred azcore.TokenCredential env env.Interface azEnv *azureclient.AROEnvironment platformIdentities map[string]api.PlatformWorkloadIdentity platformIdentitiesActionsMap map[string][]string virtualNetworks virtualNetworksGetClient diskEncryptionSets compute.DiskEncryptionSetsClient resourceSkusClient compute.ResourceSkusClient spNetworkUsage armnetwork.UsagesClient loadBalancerBackendAddressPoolsClient network.LoadBalancerBackendAddressPoolsClient pdpClient client.RemotePDPClient } type AuthorizerType string const ( AuthorizerFirstParty AuthorizerType = "resource provider" AuthorizerClusterServicePrincipal AuthorizerType = "cluster" AuthorizerClusterUserAssignedIdentity AuthorizerType = "cluster user assigned identity" AuthorizerWorkloadIdentity AuthorizerType = "platform workload identity" ) func NewValidator( log *logrus.Entry, env env.Interface, azEnv *azureclient.AROEnvironment, subscriptionID string, authorizer autorest.Authorizer, appID *string, authorizerType AuthorizerType, cred azcore.TokenCredential, pdpClient client.RemotePDPClient, ) (Dynamic, error) { options := azEnv.ArmClientOptions() usagesClient, err := armnetwork.NewUsagesClient(subscriptionID, cred, options) if err != nil { return nil, err } virtualNetworksClient, err := armnetwork.NewVirtualNetworksClient(subscriptionID, cred, options) if err != nil { return nil, err } return &dynamic{ log: log, appID: appID, authorizerType: authorizerType, env: env, azEnv: azEnv, checkAccessSubjectInfoCred: cred, spNetworkUsage: usagesClient, virtualNetworks: newVirtualNetworksCache(virtualNetworksClient), diskEncryptionSets: compute.NewDiskEncryptionSetsClientWithAROEnvironment(azEnv, subscriptionID, authorizer), resourceSkusClient: compute.NewResourceSkusClient(azEnv, subscriptionID, authorizer), pdpClient: pdpClient, loadBalancerBackendAddressPoolsClient: network.NewLoadBalancerBackendAddressPoolsClient(azEnv, subscriptionID, authorizer), }, nil } func NewServicePrincipalValidator( log *logrus.Entry, azEnv *azureclient.AROEnvironment, authorizerType AuthorizerType, ) ServicePrincipalValidator { return &dynamic{ log: log, authorizerType: authorizerType, azEnv: azEnv, } } func (dv *dynamic) ValidateVnet( ctx context.Context, location string, subnets []Subnet, additionalCIDRs ...string, ) error { if len(subnets) == 0 { return fmt.Errorf("no subnets provided") } // each subnet is threated individually as it would be from the different vnet // During cluster runtime worker profile gets enriched and contains multiple // duplicate values for multiple worker pools. We care only about // unique subnet value in the functions below. subnets = uniqueSubnetSlice(subnets) // get unique vnets from subnets vnets := make(map[string]azure.Resource) for _, s := range subnets { vnetID, _, err := apisubnet.Split(s.ID) if err != nil { return err } vnetr, err := azure.ParseResourceID(vnetID) if err != nil { return err } vnets[strings.ToLower(vnetID)] = vnetr } // validate at vnet level for _, vnet := range vnets { err := dv.validateVnetPermissions(ctx, vnet) if err != nil { return err } err = dv.validateVnetLocation(ctx, vnet, location) if err != nil { return err } } // validate at subnets level for _, s := range subnets { err := dv.validateSubnetPermissions(ctx, s) if err != nil { return err } } for _, s := range subnets { err := dv.validateRouteTablePermissions(ctx, s) if err != nil { return err } } for _, s := range subnets { err := dv.validateNatGatewayPermissions(ctx, s) if err != nil { return err } } return dv.validateCIDRRanges(ctx, subnets, additionalCIDRs...) } func (dv *dynamic) validateVnetPermissions(ctx context.Context, vnet azure.Resource) error { dv.log.Printf("validateVnetPermissions") errCode := api.CloudErrorCodeInvalidResourceProviderPermissions if dv.authorizerType == AuthorizerClusterServicePrincipal { errCode = api.CloudErrorCodeInvalidServicePrincipalPermissions } operatorName, err := dv.validateActions(ctx, &vnet, []string{ "Microsoft.Network/virtualNetworks/join/action", "Microsoft.Network/virtualNetworks/read", "Microsoft.Network/virtualNetworks/write", }) var noPermissionsErr *api.CloudError if err != nil { if dv.authorizerType == AuthorizerWorkloadIdentity { noPermissionsErr = api.NewCloudError( http.StatusBadRequest, api.CloudErrorCodeInvalidWorkloadIdentityPermissions, "", fmt.Sprintf( errMsgWIHasNoRequiredPermissionsOnVNet, *operatorName, vnet.String(), )) } else { noPermissionsErr = api.NewCloudError( http.StatusBadRequest, errCode, "", fmt.Sprintf( errMsgSPHasNoRequiredPermissionsOnVNet, dv.authorizerType, *dv.appID, vnet.String(), )) } } if err == wait.ErrWaitTimeout { return noPermissionsErr } if detailedErr, ok := err.(autorest.DetailedError); ok { dv.log.Error(detailedErr) switch detailedErr.StatusCode { case http.StatusNotFound: return api.NewCloudError( http.StatusBadRequest, api.CloudErrorCodeInvalidLinkedVNet, "", fmt.Sprintf( errMsgVnetNotFound, vnet.String(), )) case http.StatusForbidden: noPermissionsErr.Message = fmt.Sprintf( "%s\nOriginal error message: %s", noPermissionsErr.Message, detailedErr.Message, ) return noPermissionsErr } } return err } func (dv *dynamic) validateSubnetPermissions(ctx context.Context, s Subnet) error { dv.log.Printf("validateSubnetPermissions") vnetID, _, err := apisubnet.Split(s.ID) if err != nil { return err } vnetr, err := azure.ParseResourceID(vnetID) if err != nil { return err } subnetr, err := azure.ParseResourceID(s.ID) if err != nil { return err } // we need to explicitly set the resource type to virtualNetworks/{vnetID}/subnets, as the // ParseResourceID function does not properly handle parsing child resources (e.g. // VNET = parent, subnet = child) and gives the incorrect resource type, effectively // giving the incorrect resource ID which causes the validateActions method to fail. subnetr.ResourceType = fmt.Sprintf("%s/%s/%s", vnetr.ResourceType, vnetr.ResourceName, "subnets") errCode := api.CloudErrorCodeInvalidResourceProviderPermissions if dv.authorizerType == AuthorizerClusterServicePrincipal { errCode = api.CloudErrorCodeInvalidServicePrincipalPermissions } operatorName, err := dv.validateActions(ctx, &subnetr, []string{ "Microsoft.Network/virtualNetworks/subnets/join/action", "Microsoft.Network/virtualNetworks/subnets/read", "Microsoft.Network/virtualNetworks/subnets/write", }) var noPermissionsErr *api.CloudError if err != nil { if dv.authorizerType == AuthorizerWorkloadIdentity { noPermissionsErr = api.NewCloudError( http.StatusBadRequest, api.CloudErrorCodeInvalidWorkloadIdentityPermissions, "", fmt.Sprintf( errMsgWIHasNoRequiredPermissionsOnSubnet, *operatorName, subnetr.String(), )) } else { noPermissionsErr = api.NewCloudError( http.StatusBadRequest, errCode, "", fmt.Sprintf( errMsgSPHasNoRequiredPermissionsOnSubnet, dv.authorizerType, *dv.appID, subnetr.String(), )) } } if err == wait.ErrWaitTimeout { return noPermissionsErr } if detailedErr, ok := err.(autorest.DetailedError); ok { dv.log.Error(detailedErr) switch detailedErr.StatusCode { case http.StatusNotFound: return api.NewCloudError( http.StatusBadRequest, api.CloudErrorCodeInvalidLinkedSubnet, "", fmt.Sprintf( errMsgSubnetNotFound, subnetr.String(), )) case http.StatusForbidden: noPermissionsErr.Message = fmt.Sprintf( "%s\nOriginal error message: %s", noPermissionsErr.Message, detailedErr.Message, ) return noPermissionsErr } } return err } // validateRouteTablesPermissions will validate permissions on provided subnet func (dv *dynamic) validateRouteTablePermissions(ctx context.Context, s Subnet) error { dv.log.Printf("validateRouteTablePermissions") vnetID, _, err := apisubnet.Split(s.ID) if err != nil { return err } vnetr, err := azure.ParseResourceID(vnetID) if err != nil { return err } vnet, err := dv.virtualNetworks.Get(ctx, vnetr.ResourceGroup, vnetr.ResourceName, nil) if err != nil { return err } rtID, err := getRouteTableID(&vnet.VirtualNetwork, s.ID) if err != nil || rtID == "" { // error or no route table return err } rtr, err := azure.ParseResourceID(rtID) if err != nil { return err } errCode := api.CloudErrorCodeInvalidResourceProviderPermissions if dv.authorizerType == AuthorizerClusterServicePrincipal { errCode = api.CloudErrorCodeInvalidServicePrincipalPermissions } operatorName, err := dv.validateActions(ctx, &rtr, []string{ "Microsoft.Network/routeTables/join/action", "Microsoft.Network/routeTables/read", "Microsoft.Network/routeTables/write", }) if err == wait.ErrWaitTimeout { if dv.authorizerType == AuthorizerWorkloadIdentity { return api.NewCloudError( http.StatusBadRequest, api.CloudErrorCodeInvalidWorkloadIdentityPermissions, "", fmt.Sprintf( errMsgWIHasNoRequiredPermissionsOnRT, *operatorName, rtID, )) } return api.NewCloudError( http.StatusBadRequest, errCode, "", fmt.Sprintf( errMsgSPHasNoRequiredPermissionsOnRT, dv.authorizerType, rtID, )) } if detailedErr, ok := err.(autorest.DetailedError); ok && detailedErr.StatusCode == http.StatusNotFound { return api.NewCloudError( http.StatusBadRequest, api.CloudErrorCodeInvalidLinkedRouteTable, "", fmt.Sprintf( errMsgRTNotFound, rtID, )) } return err } // validateNatGatewayPermissions will validate permissions on provided subnet func (dv *dynamic) validateNatGatewayPermissions(ctx context.Context, s Subnet) error { dv.log.Printf("validateNatGatewayPermissions") vnetID, _, err := apisubnet.Split(s.ID) if err != nil { return err } vnetr, err := azure.ParseResourceID(vnetID) if err != nil { return err } vnet, err := dv.virtualNetworks.Get(ctx, vnetr.ResourceGroup, vnetr.ResourceName, nil) if err != nil { return err } ngID, err := getNatGatewayID(&vnet.VirtualNetwork, s.ID) if err != nil { return err } if ngID == "" { // empty nat gateway return nil } ngr, err := azure.ParseResourceID(ngID) if err != nil { return err } errCode := api.CloudErrorCodeInvalidResourceProviderPermissions if dv.authorizerType == AuthorizerClusterServicePrincipal { errCode = api.CloudErrorCodeInvalidServicePrincipalPermissions } operatorName, err := dv.validateActions(ctx, &ngr, []string{ "Microsoft.Network/natGateways/join/action", "Microsoft.Network/natGateways/read", "Microsoft.Network/natGateways/write", }) if err == wait.ErrWaitTimeout { if dv.authorizerType == AuthorizerWorkloadIdentity { return api.NewCloudError( http.StatusBadRequest, api.CloudErrorCodeInvalidWorkloadIdentityPermissions, "", fmt.Sprintf( errMsgWIHasNoRequiredPermissionsOnNatGW, *operatorName, ngID, )) } return api.NewCloudError( http.StatusBadRequest, errCode, "", fmt.Sprintf( errMsgSPHasNoRequiredPermissionsOnNatGW, dv.authorizerType, ngID, )) } if detailedErr, ok := err.(autorest.DetailedError); ok && detailedErr.StatusCode == http.StatusNotFound { return api.NewCloudError( http.StatusBadRequest, api.CloudErrorCodeInvalidLinkedNatGateway, "", fmt.Sprintf( errMsgNatGWNotFound, ngID, )) } return err } // validateActionsByOID creates a closure with oid to call usingCheckAccessV2 for checking SP/MI has actions allowed on a resource // oid is nil(fetched from access token) when validating FPSP, Non-MIWI Cluster Service Principal and MIWI Cluster User Assigned Managed Identity // oid is passed only for validating MIWI Cluster Platform Managed Identity func (dv *dynamic) validateActionsByOID(ctx context.Context, r *azure.Resource, actions []string, oid *string) error { // ARM has a 5 minute cache around role assignment creation, so wait one minute longer timeoutCtx, cancel := context.WithTimeout(ctx, 6*time.Minute) defer cancel() c := closure{dv: dv, ctx: ctx, resource: r, actions: actions, oid: oid} return wait.PollImmediateUntil(30*time.Second, c.usingCheckAccessV2, timeoutCtx.Done()) } // closure is the closure used in PollImmediateUntil's ConditionalFunc type closure struct { dv *dynamic ctx context.Context resource *azure.Resource actions []string oid *string jwtToken *string } func (c *closure) checkAccessAuthReqToken() error { scope := c.dv.env.Environment().ResourceManagerEndpoint + "/.default" t, err := c.dv.checkAccessSubjectInfoCred.GetToken(c.ctx, policy.TokenRequestOptions{Scopes: []string{scope}}) if err != nil { c.dv.log.Error("Unable to get the token from AAD: ", err) return err } claims, err := token.ExtractClaims(t.Token) if err != nil { c.dv.log.Error("Unable to get the oid from token: ", err) return err } c.oid = &claims.ObjectId c.jwtToken = &t.Token return nil } // usingCheckAccessV2 uses the new RBAC checkAccessV2 API func (c closure) usingCheckAccessV2() (result bool, err error) { c.dv.log.Info("validateActions with CheckAccessV2") var authReq *client.AuthorizationRequest //ensure token and oid is available during retries if c.dv.authorizerType != AuthorizerWorkloadIdentity { if c.jwtToken == nil || c.oid == nil { if err = c.checkAccessAuthReqToken(); err != nil { return false, err } } authReq, err = c.dv.pdpClient.CreateAuthorizationRequest(c.resource.String(), c.actions, *c.jwtToken) if err != nil { c.dv.log.Error("Unexpected error when creating CheckAccessV2 AuthorizationRequest: ", err) return false, err } } else { authReq = createAuthorizationRequestForPlatformWorkloadIdentity(*c.oid, c.resource.String(), c.actions...) } results, err := c.dv.pdpClient.CheckAccess(c.ctx, *authReq) if err != nil { c.dv.log.Error("Unexpected error when calling CheckAccessV2: ", err) return false, err } if results == nil { c.dv.log.Info("nil response returned from CheckAccessV2") return false, nil } actionsToFind := map[string]struct{}{} for _, action := range c.actions { actionsToFind[action] = struct{}{} } for _, result := range results.Value { _, ok := actionsToFind[result.ActionId] if ok { delete(actionsToFind, result.ActionId) if result.AccessDecision != client.Allowed { return false, nil } } } if len(actionsToFind) > 0 { c.dv.log.Infof("The result didn't include permissions %v for object ID: %s", actionsToFind, *c.oid) return false, nil } return true, nil } func (dv *dynamic) validateCIDRRanges(ctx context.Context, subnets []Subnet, additionalCIDRs ...string) error { dv.log.Print("ValidateCIDRRanges") // During cluster runtime they get enriched and contains multiple // duplicate values for multiple worker pools. CIDRRange validation // only cares about unique CIDR ranges. subnets = uniqueSubnetSlice(subnets) var CIDRArray []*net.IPNet // unique names of subnets from all node pools for _, s := range subnets { vnetID, _, err := apisubnet.Split(s.ID) if err != nil { return err } vnetr, err := azure.ParseResourceID(vnetID) if err != nil { return err } vnet, err := dv.virtualNetworks.Get(ctx, vnetr.ResourceGroup, vnetr.ResourceName, nil) if err != nil { return err } s, err := findSubnet(&vnet.VirtualNetwork, s.ID) if err != nil { return err } // Validate the CIDR of AddressPrefix or AddressPrefixes, whichever is defined if s.Properties.AddressPrefix == nil { for _, address := range s.Properties.AddressPrefixes { _, net, err := net.ParseCIDR(*address) if err != nil { return err } CIDRArray = append(CIDRArray, net) } } else { _, net, err := net.ParseCIDR(*s.Properties.AddressPrefix) if err != nil { return err } CIDRArray = append(CIDRArray, net) } } for _, c := range additionalCIDRs { _, cidr, err := net.ParseCIDR(c) if err != nil { return err } CIDRArray = append(CIDRArray, cidr) } err := cidr.VerifyNoOverlap(CIDRArray, &net.IPNet{IP: net.IPv4zero, Mask: net.IPMask(net.IPv4zero)}) if err != nil { return api.NewCloudError( http.StatusBadRequest, api.CloudErrorCodeInvalidLinkedVNet, "", fmt.Sprintf( errMsgCIDROverlaps, err, )) } return nil } func (dv *dynamic) validateVnetLocation(ctx context.Context, vnetr azure.Resource, location string) error { dv.log.Print("validateVnetLocation") vnet, err := dv.virtualNetworks.Get(ctx, vnetr.ResourceGroup, vnetr.ResourceName, nil) if err != nil { return err } if !strings.EqualFold(*vnet.Location, location) { return api.NewCloudError( http.StatusBadRequest, api.CloudErrorCodeInvalidLinkedVNet, "", fmt.Sprintf( errMsgInvalidVNetLocation, *vnet.Location, location, )) } return nil } func (dv *dynamic) createSubnetMapByID(ctx context.Context, subnets []Subnet) (map[string]*sdknetwork.Subnet, error) { if len(subnets) == 0 { return nil, fmt.Errorf("no subnets found") } subnetByID := make(map[string]*sdknetwork.Subnet) for _, s := range subnets { vnetID, _, err := apisubnet.Split(s.ID) if err != nil { return nil, err } vnetr, err := azure.ParseResourceID(vnetID) if err != nil { return nil, err } vnet, err := dv.virtualNetworks.Get(ctx, vnetr.ResourceGroup, vnetr.ResourceName, nil) if err != nil { return nil, err } ss, err := findSubnet(&vnet.VirtualNetwork, s.ID) if err != nil { return nil, err } if ss == nil { return nil, api.NewCloudError( http.StatusBadRequest, api.CloudErrorCodeInvalidLinkedVNet, s.Path, fmt.Sprintf( errMsgSubnetNotFound, s.ID, )) } subnetByID[s.ID] = ss } return subnetByID, nil } // checkPreconfiguredNSG checks whether all the subnets have an NSG attached. // when the PreconfigureNSG feature flag is on and not all subnets are attached, // it returns an error. func (dv *dynamic) checkPreconfiguredNSG(subnetByID map[string]*sdknetwork.Subnet) error { var attached int for _, subnet := range subnetByID { if subnetHasNSGAttached(subnet) { attached++ } } // all subnets have an attached NSG if attached == len(subnetByID) { dv.log.Info("all subnets are attached, BYO NSG") return nil // correct setup by customer } return &api.CloudError{ StatusCode: http.StatusBadRequest, CloudErrorBody: &api.CloudErrorBody{ Code: api.CloudErrorCodeInvalidLinkedVNet, Message: errMsgNSGNotProperlyAttached, }, } } func (dv *dynamic) ValidateSubnets(ctx context.Context, oc *api.OpenShiftCluster, subnets []Subnet) error { dv.log.Printf("validateSubnet") subnetByID, err := dv.createSubnetMapByID(ctx, subnets) if err != nil { return err } if oc.Properties.ProvisioningState == api.ProvisioningStateCreating { if oc.Properties.NetworkProfile.PreconfiguredNSG == api.PreconfiguredNSGEnabled { dv.log.Info("cluster creation with preconfigured-nsg") err = dv.checkPreconfiguredNSG(subnetByID) if err != nil { return err } } } // we're parsing through the subnets slice, not the map because we'll return consistent error messages on creation for _, s := range subnets { ss := subnetByID[s.ID] if oc.Properties.ProvisioningState == api.ProvisioningStateCreating { if subnetHasNSGAttached(ss) && oc.Properties.NetworkProfile.PreconfiguredNSG != api.PreconfiguredNSGEnabled { expectedNsgID, err := apisubnet.NetworkSecurityGroupID(oc, s.ID) if err != nil { return err } if !isTheSameNSG(*ss.Properties.NetworkSecurityGroup.ID, expectedNsgID) { return api.NewCloudError( http.StatusBadRequest, api.CloudErrorCodeInvalidLinkedVNet, s.Path, fmt.Sprintf(errMsgNSGAttached, s.ID)) } } } else { nsgID, err := apisubnet.NetworkSecurityGroupID(oc, *ss.ID) if err != nil { return err } if oc.Properties.NetworkProfile.PreconfiguredNSG == api.PreconfiguredNSGDisabled { if !subnetHasNSGAttached(ss) || !isTheSameNSG(*ss.Properties.NetworkSecurityGroup.ID, nsgID) { return api.NewCloudError( http.StatusBadRequest, api.CloudErrorCodeInvalidLinkedVNet, s.Path, fmt.Sprintf( errMsgOriginalNSGNotAttached, s.ID, nsgID, )) } } else { if !subnetHasNSGAttached(ss) { return api.NewCloudError( http.StatusBadRequest, api.CloudErrorCodeInvalidLinkedVNet, s.Path, fmt.Sprintf( errMsgNSGNotAttached, s.ID, )) } } } if ss.Properties == nil || ss.Properties.ProvisioningState == nil || *ss.Properties.ProvisioningState != sdknetwork.ProvisioningStateSucceeded { return api.NewCloudError( http.StatusBadRequest, api.CloudErrorCodeInvalidLinkedVNet, s.Path, fmt.Sprintf( errMsgSubnetNotInSucceededState, s.ID, )) } // Handle both addressPrefix & addressPrefixes if ss.Properties.AddressPrefix == nil { for _, address := range ss.Properties.AddressPrefixes { if err = validateSubnetSize(s, *address); err != nil { return err } } } else { if err = validateSubnetSize(s, *ss.Properties.AddressPrefix); err != nil { return err } } } return nil } // validateSubnetSize checks if the subnet mask is >27, and returns // an error if so as it is too small for OCP func validateSubnetSize(s Subnet, address string) error { _, net, err := net.ParseCIDR(address) if err != nil { return err } ones, _ := net.Mask.Size() if ones > minimumSubnetMaskSize { return api.NewCloudError( http.StatusBadRequest, api.CloudErrorCodeInvalidLinkedVNet, s.Path, fmt.Sprintf( errMsgSubnetInvalidSize, s.ID, )) } return nil } func (dv *dynamic) ValidatePreConfiguredNSGs(ctx context.Context, oc *api.OpenShiftCluster, subnets []Subnet) error { dv.log.Print("ValidatePreConfiguredNSGs") if oc.Properties.NetworkProfile.PreconfiguredNSG != api.PreconfiguredNSGEnabled { return nil // exit early } subnetByID, err := dv.createSubnetMapByID(ctx, subnets) if err != nil { return err } for _, s := range subnetByID { nsgID := s.Properties.NetworkSecurityGroup.ID if nsgID == nil || *nsgID == "" { return api.NewCloudError( http.StatusBadRequest, api.CloudErrorCodeNotFound, "", errMsgNSGNotProperlyAttached, ) } if err := dv.validateNSGPermissions(ctx, *nsgID); err != nil { return err } } return nil } // validateActions calls validateActionsByOID with object ID in case of MIWI cluster otherwise without object ID func (dv *dynamic) validateActions(ctx context.Context, r *azure.Resource, actions []string) (*string, error) { if dv.platformIdentities != nil { for name, platformIdentity := range dv.platformIdentities { actionsToValidate := stringutils.GroupsIntersect(actions, dv.platformIdentitiesActionsMap[name]) if len(actionsToValidate) > 0 { if err := dv.validateActionsByOID(ctx, r, actionsToValidate, &platformIdentity.ObjectID); err != nil { return &name, err } } } } else { if err := dv.validateActionsByOID(ctx, r, actions, nil); err != nil { return nil, err } } return nil, nil } func (dv *dynamic) validateNSGPermissions(ctx context.Context, nsgID string) error { nsg, err := azure.ParseResourceID(nsgID) if err != nil { return err } operatorName, err := dv.validateActions(ctx, &nsg, []string{ "Microsoft.Network/networkSecurityGroups/join/action", }) if err == wait.ErrWaitTimeout { errCode := api.CloudErrorCodeInvalidResourceProviderPermissions if dv.authorizerType == AuthorizerClusterServicePrincipal { errCode = api.CloudErrorCodeInvalidServicePrincipalPermissions } else if dv.authorizerType == AuthorizerWorkloadIdentity { return api.NewCloudError( http.StatusBadRequest, api.CloudErrorCodeInvalidWorkloadIdentityPermissions, "", fmt.Sprintf( errMsgWIHasNoRequiredPermissionsOnNSG, *operatorName, nsgID, )) } return api.NewCloudError( http.StatusBadRequest, errCode, "", fmt.Sprintf( errMsgSPHasNoRequiredPermissionsOnNSG, dv.authorizerType, *dv.appID, nsgID, )) } return err } func isTheSameNSG(found, inDB string) bool { return strings.EqualFold(found, inDB) } func subnetHasNSGAttached(subnet *sdknetwork.Subnet) bool { return subnet.Properties.NetworkSecurityGroup != nil && subnet.Properties.NetworkSecurityGroup.ID != nil } func getRouteTableID(vnet *sdknetwork.VirtualNetwork, subnetID string) (string, error) { s, err := findSubnet(vnet, subnetID) if err != nil { return "", err } if s == nil || s.Properties.RouteTable == nil { return "", nil } return *s.Properties.RouteTable.ID, nil } func getNatGatewayID(vnet *sdknetwork.VirtualNetwork, subnetID string) (string, error) { s, err := findSubnet(vnet, subnetID) if err != nil { return "", err } if s == nil || s.Properties.NatGateway == nil { return "", nil } return *s.Properties.NatGateway.ID, nil } func findSubnet(vnet *sdknetwork.VirtualNetwork, subnetID string) (*sdknetwork.Subnet, error) { if vnet.Properties.Subnets != nil { for _, s := range vnet.Properties.Subnets { if strings.EqualFold(*s.ID, subnetID) { return s, nil } } } return nil, api.NewCloudError( http.StatusBadRequest, api.CloudErrorCodeInvalidLinkedVNet, "", fmt.Sprintf( errMsgSubnetNotFound, subnetID, )) } // uniqueSubnetSlice returns string subnets with unique values only func uniqueSubnetSlice(slice []Subnet) []Subnet { keys := make(map[string]bool) list := []Subnet{} for _, entry := range slice { if _, value := keys[strings.ToLower(entry.ID)]; !value { keys[strings.ToLower(entry.ID)] = true list = append(list, entry) } } return list } func createAuthorizationRequestForPlatformWorkloadIdentity(subject, resourceId string, actions ...string) *client.AuthorizationRequest { actionInfos := []client.ActionInfo{} for _, action := range actions { actionInfos = append(actionInfos, client.ActionInfo{Id: action}) } return &client.AuthorizationRequest{ Subject: client.SubjectInfo{ Attributes: client.SubjectAttributes{ ObjectId: subject, }, }, Actions: actionInfos, Resource: client.ResourceInfo{ Id: resourceId, }, } }