@@ -7,9 +7,22 @@ package customizations
77
88import (
99 "context"
10+ "fmt"
1011 "strings"
12+ "sync"
13+
14+ "github.com/Azure/azure-sdk-for-go/sdk/azcore/arm"
15+ "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/authorization/armauthorization/v2"
16+ "github.com/go-logr/logr"
17+ "github.com/rotisserie/eris"
18+ "sigs.k8s.io/controller-runtime/pkg/conversion"
1119
1220 api "github.com/Azure/azure-service-operator/v2/api/authorization/v1api20220401"
21+ storage "github.com/Azure/azure-service-operator/v2/api/authorization/v1api20220401/storage"
22+ "github.com/Azure/azure-service-operator/v2/internal/genericarmclient"
23+ "github.com/Azure/azure-service-operator/v2/internal/reflecthelpers"
24+ "github.com/Azure/azure-service-operator/v2/internal/resolver"
25+ "github.com/Azure/azure-service-operator/v2/internal/util/kubeclient"
1326 "github.com/Azure/azure-service-operator/v2/pkg/genruntime"
1427 "github.com/Azure/azure-service-operator/v2/pkg/genruntime/extensions"
1528)
@@ -43,3 +56,129 @@ func (extension *RoleAssignmentExtension) Import(
4356
4457 return result , nil
4558}
59+
60+ var _ extensions.ARMResourceModifier = & RoleAssignmentExtension {}
61+
62+ func (extension * RoleAssignmentExtension ) ModifyARMResource (
63+ ctx context.Context ,
64+ armClient * genericarmclient.GenericClient ,
65+ armObj genruntime.ARMResource ,
66+ obj genruntime.ARMMetaObject ,
67+ kubeClient kubeclient.Client ,
68+ resolver * resolver.Resolver ,
69+ log logr.Logger ,
70+ ) (genruntime.ARMResource , error ) {
71+ ra , ok := obj .(* storage.RoleAssignment )
72+ if ! ok {
73+ return nil , eris .Errorf (
74+ "Cannot run RoleAssignmentExtension.ModifyARMResource() with unexpected resource type %T" ,
75+ obj )
76+ }
77+
78+ // Type assert that we are the hub type. This will fail to compile if
79+ // the hub type has been changed but this extension has not been updated to match
80+ var _ conversion.Hub = ra
81+
82+ // If the specified role definition uses a well known name, look it up
83+ roleDefinitionName := ra .Spec .RoleDefinitionReference .WellKnownName
84+ if roleDefinitionName != "" {
85+ err := ensureBuiltInRoleDefinitionsLoaded (ctx , armClient )
86+ if err != nil {
87+ return nil , eris .Wrapf (err , "loading built in role definitions to resolve %q" , roleDefinitionName )
88+ }
89+
90+ roleDefinitionId , err := resolveBuiltInRoleDefinition (roleDefinitionName , armObj )
91+ if err != nil {
92+ return nil , eris .Wrapf (err , "resolving built in role definition %q" , roleDefinitionName )
93+ }
94+
95+ if roleDefinitionId != roleDefinitionName {
96+ log .V (1 ).Info ("Resolved built-in role" , "roleName" , roleDefinitionName , "roleId" , roleDefinitionId )
97+
98+ spec := armObj .Spec ()
99+ err = reflecthelpers .SetProperty (spec , "Properties.RoleDefinitionId" , & roleDefinitionId )
100+ if err != nil {
101+ return nil , eris .Wrapf (err , "error setting RoleDefinitionId to %s" , roleDefinitionId )
102+ }
103+
104+ return armObj , nil
105+ }
106+ }
107+
108+ return armObj , nil
109+ }
110+
111+ var (
112+ // builtInRoleDefinitions is a cache of all known built-in role definitions, keyed by
113+ // lower case role name. At the time of writing there are ~700 such roles, so we
114+ // pre-initialize the map to sufficient capacity to accommodate those and some modest growth.
115+ builtInRoleDefinitions map [string ]string = make (map [string ]string , 800 )
116+
117+ // builtInRoleDefinitionsLock protects access to builtInRoleDefinitions
118+ builtInRoleDefinitionsLock sync.RWMutex
119+ )
120+
121+ // ensureBuiltInRoleDefinitionsLoaded loads the built-in role definitions into memory if not already loaded.
122+ // We load them once, and then keep them for the lifetime of the pod.
123+ // Given the list of built-in role definitions changes very slowly, this seems reasonable.
124+ func ensureBuiltInRoleDefinitionsLoaded (
125+ ctx context.Context ,
126+ armClient * genericarmclient.GenericClient ,
127+ ) error {
128+ builtInRoleDefinitionsLock .Lock ()
129+ defer builtInRoleDefinitionsLock .Unlock ()
130+
131+ // Short circuit if already loaded
132+ if len (builtInRoleDefinitions ) > 0 {
133+ return nil
134+ }
135+
136+ cl , err := armauthorization .NewRoleDefinitionsClient (armClient .Creds (), armClient .ClientOptions ())
137+ if err != nil {
138+ return eris .Wrap (err , "creating client to load built-in role definitions" )
139+ }
140+
141+ pager := cl .NewListPager ("/" , nil )
142+ for pager .More () {
143+ page , err := pager .NextPage (ctx )
144+ if err != nil {
145+ clear (builtInRoleDefinitions ) // ensure we'll try again next time
146+ return eris .Wrap (err , "loading built-in role definitions" )
147+ }
148+
149+ for _ , role := range page .Value {
150+ if * role .Properties .RoleType == "BuiltInRole" {
151+ builtInRoleDefinitions [strings .ToLower (* role .Properties .RoleName )] = * role .Name
152+ }
153+ }
154+ }
155+
156+ return nil
157+ }
158+
159+ func resolveBuiltInRoleDefinition (
160+ roleDefinitionName string ,
161+ armObj genruntime.ARMResource ,
162+ ) (string , error ) {
163+ builtInRoleDefinitionsLock .RLock ()
164+ defer builtInRoleDefinitionsLock .RUnlock ()
165+
166+ roleId , ok := builtInRoleDefinitions [strings .ToLower (roleDefinitionName )]
167+ if ! ok {
168+ // If we can't resolve, it, leave it intact
169+ return roleDefinitionName , nil
170+ }
171+
172+ // We need the subscription ID from the resource to construct the ARM ID for a well known role
173+ roleARMId , err := arm .ParseResourceID (armObj .GetID ())
174+ if err != nil {
175+ return "" , eris .Wrapf (err , "failed to parse the ARM ResourceId for %s" , armObj .GetID ())
176+ }
177+
178+ armID := fmt .Sprintf (
179+ "/subscriptions/%s/providers/Microsoft.Authorization/roleDefinitions/%s" ,
180+ roleARMId .SubscriptionID ,
181+ roleId )
182+
183+ return armID , nil
184+ }
0 commit comments