Skip to content

Commit 150cd3e

Browse files
authored
Feat/workload identity support (#45)
* feat: add workload identity authentication support Add support for Azure Workload Identity authentication alongside existing service principal authentication for Microsoft Graph API access. * comments and empty string check * test credential type choice * remove debug println
1 parent e1083b8 commit 150cd3e

File tree

8 files changed

+505
-18
lines changed

8 files changed

+505
-18
lines changed

README.md

Lines changed: 108 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,12 @@ spec:
2828
2929
### Azure Credentials
3030
31+
The service principal needs the following Microsoft Graph API permissions:
32+
- User.Read.All (for user validation)
33+
- Group.Read.All (for group operations)
34+
- Application.Read.All (for service principal details)
35+
36+
#### Client Secret Credentials
3137
Create an Azure service principal with appropriate permissions to access Microsoft Graph API:
3238
3339
```yaml
@@ -47,10 +53,81 @@ stringData:
4753
}
4854
```
4955
50-
The service principal needs the following Microsoft Graph API permissions:
51-
- User.Read.All (for user validation)
52-
- Group.Read.All (for group operations)
53-
- Application.Read.All (for service principal details)
56+
#### Workload Identity Credentials
57+
AKS cluster needs to have workload identity enabled.
58+
The managed identity needs to have the Federated Identity Credential created: https://azure.github.io/azure-workload-identity/docs/topics/federated-identity-credential.html.
59+
60+
##### Credentials secret:
61+
```yaml
62+
apiVersion: v1
63+
kind: Secret
64+
metadata:
65+
name: azure-account-creds
66+
namespace: crossplane-system
67+
type: Opaque
68+
stringData:
69+
credentials: |
70+
{
71+
"clientId": "your-client-id", # optional
72+
"tenantId": "your-tenant-id", # optional
73+
"federatedTokenFile": "/var/run/secrets/azure/tokens/azure-identity-token"
74+
}
75+
```
76+
77+
##### Function
78+
```yaml
79+
apiVersion: pkg.crossplane.io/v1
80+
kind: Function
81+
metadata:
82+
name: upbound-function-msgraph
83+
spec:
84+
package: xpkg.upbound.io/upbound/function-msgraph:v0.2.0
85+
runtimeConfigRef:
86+
apiVersion: pkg.crossplane.io/v1beta1
87+
kind: DeploymentRuntimeConfig
88+
name: upbound-function-msgraph
89+
```
90+
91+
##### DeploymentRuntimeConfig
92+
```yaml
93+
apiVersion: pkg.crossplane.io/v1beta1
94+
kind: DeploymentRuntimeConfig
95+
metadata:
96+
name: upbound-function-msgraph
97+
spec:
98+
deploymentTemplate:
99+
spec:
100+
selector:
101+
matchLabels:
102+
azure.workload.identity/use: "true"
103+
pkg.crossplane.io/function: "upbound-function-msgraph"
104+
template:
105+
metadata:
106+
labels:
107+
azure.workload.identity/use: "true"
108+
pkg.crossplane.io/function: "upbound-function-msgraph"
109+
spec:
110+
containers:
111+
- name: package-runtime
112+
volumeMounts:
113+
- mountPath: /var/run/secrets/azure/tokens
114+
name: azure-identity-token
115+
readOnly: true
116+
serviceAccountName: "upbound-function-msgraph"
117+
volumes:
118+
- name: azure-identity-token
119+
projected:
120+
sources:
121+
- serviceAccountToken:
122+
audience: api://AzureADTokenExchange
123+
expirationSeconds: 3600
124+
path: azure-identity-token
125+
serviceAccountTemplate:
126+
metadata:
127+
annotations:
128+
azure.workload.identity/client-id: "your-client-id"
129+
name: "upbound-function-msgraph"
130+
```
54131
55132
## Examples
56133
@@ -198,6 +275,7 @@ spec:
198275
| `servicePrincipalsRef` | string | Reference to resolve a list of service principal names from `spec`, `status` or `context` (e.g., `spec.servicePrincipalConfig.names`) |
199276
| `target` | string | Required. Where to store the query results. Can be `status.<field>` or `context.<field>` |
200277
| `skipQueryWhenTargetHasData` | bool | Optional. When true, will skip the query if the target already has data |
278+
| `identity.type | string | Optional. Type of identity credentials to use. Valid values: `AzureServicePrincipalCredentials`, `AzureWorkloadIdentityCredentials`. Default is `AzureServicePrincipalCredentials` |
201279

202280
## Result Targets
203281

@@ -261,6 +339,32 @@ servicePrincipalsRef: "spec.servicePrincipalConfig.names" # Get service princip
261339
target: "status.servicePrincipals"
262340
```
263341

342+
## Using Different Credentials
343+
344+
### Using ServicePrincipal credentials
345+
346+
#### Explicitly
347+
```yaml
348+
apiVersion: msgraph.fn.crossplane.io/v1alpha1
349+
kind: Input
350+
identity:
351+
type: AzureServicePrincipalCredentials
352+
```
353+
354+
#### Default
355+
```yaml
356+
apiVersion: msgraph.fn.crossplane.io/v1alpha1
357+
kind: Input
358+
```
359+
360+
### Using Workload Identity Credentials
361+
```yaml
362+
apiVersion: msgraph.fn.crossplane.io/v1alpha1
363+
kind: Input
364+
identity:
365+
type: AzureWorkloadIdentityCredentials
366+
```
367+
264368
## References
265369

266370
- [Microsoft Graph API Overview](https://learn.microsoft.com/en-us/graph/api/overview?view=graph-rest-1.0)
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
apiVersion: apiextensions.crossplane.io/v1
2+
kind: Composition
3+
metadata:
4+
name: workload-identity-example
5+
# Important: This function example requires an Azure AD app registration with Microsoft Graph API permissions:
6+
# - User.Read.All
7+
# - Directory.Read.All
8+
spec:
9+
compositeTypeRef:
10+
apiVersion: example.crossplane.io/v1
11+
kind: XR
12+
mode: Pipeline
13+
pipeline:
14+
- step: validate-user
15+
functionRef:
16+
name: function-msgraph
17+
input:
18+
apiVersion: msgraph.fn.crossplane.io/v1alpha1
19+
kind: Input
20+
queryType: UserValidation
21+
# Replace these with actual users in your directory
22+
users:
23+
24+
25+
26+
target: "status.validatedUsers"
27+
skipQueryWhenTargetHasData: true
28+
identity:
29+
type: AzureWorkloadIdentityCredentials
30+
credentials:
31+
- name: azure-creds
32+
source: Secret
33+
secretRef:
34+
namespace: upbound-system
35+
name: azure-workload-identity-creds
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
apiVersion: v1
2+
stringData:
3+
credentials: |
4+
{
5+
"federatedTokenFile": "/var/run/secrets/azure/tokens/azure-identity-token"
6+
}
7+
kind: Secret
8+
metadata:
9+
name: azure-workload-identity-creds
10+
namespace: upbound-system
11+
type: Opaque

fn.go

Lines changed: 83 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,22 @@ import (
2727
"github.com/crossplane/function-sdk-go/response"
2828
)
2929

30+
var (
31+
// MSGraphScopes defines the default MS Graph scopes
32+
MSGraphScopes = []string{"https://graph.microsoft.com/.default"}
33+
)
34+
35+
const (
36+
// TenantID defines the azure credentials key for tenant id
37+
TenantID = "tenantId"
38+
// ClientID defines the azure credentials key for client id
39+
ClientID = "clientId"
40+
// ClientSecret defines the azure credentials key for client secret
41+
ClientSecret = "clientSecret"
42+
// WorkloadIdentityCredentialPath defines the azure credentials key for federated token file path
43+
WorkloadIdentityCredentialPath = "federatedTokenFile"
44+
)
45+
3046
// GraphQueryInterface defines the methods required for querying Microsoft Graph API.
3147
type GraphQueryInterface interface {
3248
graphQuery(ctx context.Context, azureCreds map[string]string, in *v1beta1.Input) (interface{}, error)
@@ -265,37 +281,91 @@ type GraphQuery struct {
265281
}
266282

267283
// createGraphClient initializes a Microsoft Graph client using the provided credentials
268-
func (g *GraphQuery) createGraphClient(azureCreds map[string]string) (*msgraphsdk.GraphServiceClient, error) {
269-
tenantID := azureCreds["tenantId"]
270-
clientID := azureCreds["clientId"]
271-
clientSecret := azureCreds["clientSecret"]
284+
func (g *GraphQuery) createGraphClient(azureCreds map[string]string, identityType v1beta1.IdentityType) (client *msgraphsdk.GraphServiceClient, err error) {
285+
authProvider := &azauth.AzureIdentityAuthenticationProvider{}
286+
287+
switch identityType {
288+
case v1beta1.IdentityTypeAzureWorkloadIdentityCredentials:
289+
authProvider, err = g.initializeWorkloadIdentityProvider(azureCreds)
290+
if err != nil {
291+
return nil, errors.Wrap(err, "failed to initialize workload identity provider")
292+
}
293+
case v1beta1.IdentityTypeAzureServicePrincipalCredentials:
294+
authProvider, err = g.initializeClientSecretProvider(azureCreds)
295+
if err != nil {
296+
return nil, errors.Wrap(err, "failed to initialize service principal provider")
297+
}
298+
}
299+
300+
// Create adapter
301+
adapter, err := msgraphsdk.NewGraphRequestAdapter(authProvider)
302+
if err != nil {
303+
return nil, errors.Wrap(err, "failed to create graph adapter")
304+
}
305+
306+
// Initialize Microsoft Graph client
307+
return msgraphsdk.NewGraphServiceClient(adapter), nil
308+
}
309+
310+
func (g *GraphQuery) initializeClientSecretProvider(azureCreds map[string]string) (*azauth.AzureIdentityAuthenticationProvider, error) {
311+
tenantID := azureCreds[TenantID]
312+
clientID := azureCreds[ClientID]
313+
clientSecret := azureCreds[ClientSecret]
272314

273315
// Create Azure credential for Microsoft Graph
274316
cred, err := azidentity.NewClientSecretCredential(tenantID, clientID, clientSecret, nil)
275317
if err != nil {
276-
return nil, errors.Wrap(err, "failed to obtain credentials")
318+
return nil, errors.Wrap(err, "failed to obtain clientsecret credentials")
277319
}
278-
279320
// Create authentication provider
280-
authProvider, err := azauth.NewAzureIdentityAuthenticationProviderWithScopes(cred, []string{"https://graph.microsoft.com/.default"})
321+
authProvider, err := azauth.NewAzureIdentityAuthenticationProviderWithScopes(cred, MSGraphScopes)
281322
if err != nil {
282323
return nil, errors.Wrap(err, "failed to create auth provider")
283324
}
284325

285-
// Create adapter
286-
adapter, err := msgraphsdk.NewGraphRequestAdapter(authProvider)
326+
return authProvider, nil
327+
}
328+
329+
func (g *GraphQuery) initializeWorkloadIdentityProvider(azureCreds map[string]string) (*azauth.AzureIdentityAuthenticationProvider, error) {
330+
options := &azidentity.WorkloadIdentityCredentialOptions{
331+
TokenFilePath: azureCreds[WorkloadIdentityCredentialPath],
332+
}
333+
334+
// Defaults to the value of the environment variable AZURE_TENANT_ID
335+
tenantID, found := azureCreds[TenantID]
336+
if found {
337+
options.TenantID = tenantID
338+
}
339+
340+
// Defaults to the value of the environment variable AZURE_CLIENT_ID
341+
clientID, found := azureCreds[ClientID]
342+
if found {
343+
options.ClientID = clientID
344+
}
345+
346+
// Create Azure credential for Microsoft Graph
347+
cred, err := azidentity.NewWorkloadIdentityCredential(options)
287348
if err != nil {
288-
return nil, errors.Wrap(err, "failed to create graph adapter")
349+
return nil, errors.Wrap(err, "failed to obtain workloadidentity credentials")
350+
}
351+
// Create authentication provider
352+
authProvider, err := azauth.NewAzureIdentityAuthenticationProviderWithScopes(cred, MSGraphScopes)
353+
if err != nil {
354+
return nil, errors.Wrap(err, "failed to create auth provider")
289355
}
290356

291-
// Initialize Microsoft Graph client
292-
return msgraphsdk.NewGraphServiceClient(adapter), nil
357+
return authProvider, nil
293358
}
294359

295360
// graphQuery is a concrete implementation that interacts with Microsoft Graph API.
296361
func (g *GraphQuery) graphQuery(ctx context.Context, azureCreds map[string]string, in *v1beta1.Input) (interface{}, error) {
362+
identityType := v1beta1.IdentityTypeAzureServicePrincipalCredentials
363+
364+
if in.Identity != nil && in.Identity.Type != "" {
365+
identityType = in.Identity.Type
366+
}
297367
// Create the Microsoft Graph client
298-
client, err := g.createGraphClient(azureCreds)
368+
client, err := g.createGraphClient(azureCreds, identityType)
299369
if err != nil {
300370
return nil, err
301371
}

0 commit comments

Comments
 (0)