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
112 changes: 108 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,12 @@ spec:

### Azure Credentials

The service principal needs the following Microsoft Graph API permissions:
- User.Read.All (for user validation)
- Group.Read.All (for group operations)
- Application.Read.All (for service principal details)

#### Client Secret Credentials
Create an Azure service principal with appropriate permissions to access Microsoft Graph API:

```yaml
Expand All @@ -47,10 +53,81 @@ stringData:
}
```

The service principal needs the following Microsoft Graph API permissions:
- User.Read.All (for user validation)
- Group.Read.All (for group operations)
- Application.Read.All (for service principal details)
#### Workload Identity Credentials
AKS cluster needs to have workload identity enabled.
The managed identity needs to have the Federated Identity Credential created: https://azure.github.io/azure-workload-identity/docs/topics/federated-identity-credential.html.

##### Credentials secret:
```yaml
apiVersion: v1
kind: Secret
metadata:
name: azure-account-creds
namespace: crossplane-system
type: Opaque
stringData:
credentials: |
{
"clientId": "your-client-id", # optional
"tenantId": "your-tenant-id", # optional
"federatedTokenFile": "/var/run/secrets/azure/tokens/azure-identity-token"
}
```

##### Function
```yaml
apiVersion: pkg.crossplane.io/v1
kind: Function
metadata:
name: upbound-function-msgraph
spec:
package: xpkg.upbound.io/upbound/function-msgraph:v0.2.0
runtimeConfigRef:
apiVersion: pkg.crossplane.io/v1beta1
kind: DeploymentRuntimeConfig
name: upbound-function-msgraph
```

##### DeploymentRuntimeConfig
```yaml
apiVersion: pkg.crossplane.io/v1beta1
kind: DeploymentRuntimeConfig
metadata:
name: upbound-function-msgraph
spec:
deploymentTemplate:
spec:
selector:
matchLabels:
azure.workload.identity/use: "true"
pkg.crossplane.io/function: "upbound-function-msgraph"
template:
metadata:
labels:
azure.workload.identity/use: "true"
pkg.crossplane.io/function: "upbound-function-msgraph"
spec:
containers:
- name: package-runtime
volumeMounts:
- mountPath: /var/run/secrets/azure/tokens
name: azure-identity-token
readOnly: true
serviceAccountName: "upbound-function-msgraph"
volumes:
- name: azure-identity-token
projected:
sources:
- serviceAccountToken:
audience: api://AzureADTokenExchange
expirationSeconds: 3600
path: azure-identity-token
serviceAccountTemplate:
metadata:
annotations:
azure.workload.identity/client-id: "your-client-id"
name: "upbound-function-msgraph"
```

## Examples

Expand Down Expand Up @@ -198,6 +275,7 @@ spec:
| `servicePrincipalsRef` | string | Reference to resolve a list of service principal names from `spec`, `status` or `context` (e.g., `spec.servicePrincipalConfig.names`) |
| `target` | string | Required. Where to store the query results. Can be `status.<field>` or `context.<field>` |
| `skipQueryWhenTargetHasData` | bool | Optional. When true, will skip the query if the target already has data |
| `identity.type | string | Optional. Type of identity credentials to use. Valid values: `AzureServicePrincipalCredentials`, `AzureWorkloadIdentityCredentials`. Default is `AzureServicePrincipalCredentials` |

## Result Targets

Expand Down Expand Up @@ -261,6 +339,32 @@ servicePrincipalsRef: "spec.servicePrincipalConfig.names" # Get service princip
target: "status.servicePrincipals"
```

## Using Different Credentials

### Using ServicePrincipal credentials

#### Explicitly
```yaml
apiVersion: msgraph.fn.crossplane.io/v1alpha1
kind: Input
identity:
type: AzureServicePrincipalCredentials
```

#### Default
```yaml
apiVersion: msgraph.fn.crossplane.io/v1alpha1
kind: Input
```

### Using Workload Identity Credentials
```yaml
apiVersion: msgraph.fn.crossplane.io/v1alpha1
kind: Input
identity:
type: AzureWorkloadIdentityCredentials
```

## References

- [Microsoft Graph API Overview](https://learn.microsoft.com/en-us/graph/api/overview?view=graph-rest-1.0)
Expand Down
35 changes: 35 additions & 0 deletions example/identity-type-workload-identity.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
apiVersion: apiextensions.crossplane.io/v1
kind: Composition
metadata:
name: workload-identity-example
# Important: This function example requires an Azure AD app registration with Microsoft Graph API permissions:
# - User.Read.All
# - Directory.Read.All
spec:
compositeTypeRef:
apiVersion: example.crossplane.io/v1
kind: XR
mode: Pipeline
pipeline:
- step: validate-user
functionRef:
name: function-msgraph
input:
apiVersion: msgraph.fn.crossplane.io/v1alpha1
kind: Input
queryType: UserValidation
# Replace these with actual users in your directory
users:
- "admin@example.onmicrosoft.com"
- "user@example.onmicrosoft.com"
- "yury@upbound.io"
target: "status.validatedUsers"
skipQueryWhenTargetHasData: true
identity:
type: AzureWorkloadIdentityCredentials
credentials:
- name: azure-creds
source: Secret
secretRef:
namespace: upbound-system
name: azure-workload-identity-creds
11 changes: 11 additions & 0 deletions example/secrets/azure-workload-identity-creds.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
apiVersion: v1
stringData:
credentials: |
{
"federatedTokenFile": "/var/run/secrets/azure/tokens/azure-identity-token"
}
kind: Secret
metadata:
name: azure-workload-identity-creds
namespace: upbound-system
type: Opaque
96 changes: 83 additions & 13 deletions fn.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,22 @@ import (
"github.com/crossplane/function-sdk-go/response"
)

var (
// MSGraphScopes defines the default MS Graph scopes
MSGraphScopes = []string{"https://graph.microsoft.com/.default"}
)

const (
// TenantID defines the azure credentials key for tenant id
TenantID = "tenantId"
// ClientID defines the azure credentials key for client id
ClientID = "clientId"
// ClientSecret defines the azure credentials key for client secret
ClientSecret = "clientSecret"
// WorkloadIdentityCredentialPath defines the azure credentials key for federated token file path
WorkloadIdentityCredentialPath = "federatedTokenFile"
)

// GraphQueryInterface defines the methods required for querying Microsoft Graph API.
type GraphQueryInterface interface {
graphQuery(ctx context.Context, azureCreds map[string]string, in *v1beta1.Input) (interface{}, error)
Expand Down Expand Up @@ -264,37 +280,91 @@ type GraphQuery struct {
}

// createGraphClient initializes a Microsoft Graph client using the provided credentials
func (g *GraphQuery) createGraphClient(azureCreds map[string]string) (*msgraphsdk.GraphServiceClient, error) {
tenantID := azureCreds["tenantId"]
clientID := azureCreds["clientId"]
clientSecret := azureCreds["clientSecret"]
func (g *GraphQuery) createGraphClient(azureCreds map[string]string, identityType v1beta1.IdentityType) (client *msgraphsdk.GraphServiceClient, err error) {
authProvider := &azauth.AzureIdentityAuthenticationProvider{}

switch identityType {
case v1beta1.IdentityTypeAzureWorkloadIdentityCredentials:
authProvider, err = g.initializeWorkloadIdentityProvider(azureCreds)
if err != nil {
return nil, errors.Wrap(err, "failed to initialize workload identity provider")
}
case v1beta1.IdentityTypeAzureServicePrincipalCredentials:
authProvider, err = g.initializeClientSecretProvider(azureCreds)
if err != nil {
return nil, errors.Wrap(err, "failed to initialize service principal provider")
}
}

// Create adapter
adapter, err := msgraphsdk.NewGraphRequestAdapter(authProvider)
if err != nil {
return nil, errors.Wrap(err, "failed to create graph adapter")
}

// Initialize Microsoft Graph client
return msgraphsdk.NewGraphServiceClient(adapter), nil
}

func (g *GraphQuery) initializeClientSecretProvider(azureCreds map[string]string) (*azauth.AzureIdentityAuthenticationProvider, error) {
tenantID := azureCreds[TenantID]
clientID := azureCreds[ClientID]
clientSecret := azureCreds[ClientSecret]

// Create Azure credential for Microsoft Graph
cred, err := azidentity.NewClientSecretCredential(tenantID, clientID, clientSecret, nil)
if err != nil {
return nil, errors.Wrap(err, "failed to obtain credentials")
return nil, errors.Wrap(err, "failed to obtain clientsecret credentials")
}

// Create authentication provider
authProvider, err := azauth.NewAzureIdentityAuthenticationProviderWithScopes(cred, []string{"https://graph.microsoft.com/.default"})
authProvider, err := azauth.NewAzureIdentityAuthenticationProviderWithScopes(cred, MSGraphScopes)
if err != nil {
return nil, errors.Wrap(err, "failed to create auth provider")
}

// Create adapter
adapter, err := msgraphsdk.NewGraphRequestAdapter(authProvider)
return authProvider, nil
}

func (g *GraphQuery) initializeWorkloadIdentityProvider(azureCreds map[string]string) (*azauth.AzureIdentityAuthenticationProvider, error) {
options := &azidentity.WorkloadIdentityCredentialOptions{
TokenFilePath: azureCreds[WorkloadIdentityCredentialPath],
}

// Defaults to the value of the environment variable AZURE_TENANT_ID
tenantID, found := azureCreds[TenantID]
if found {
options.TenantID = tenantID
}

// Defaults to the value of the environment variable AZURE_CLIENT_ID
clientID, found := azureCreds[ClientID]
if found {
options.ClientID = clientID
}

// Create Azure credential for Microsoft Graph
cred, err := azidentity.NewWorkloadIdentityCredential(options)
if err != nil {
return nil, errors.Wrap(err, "failed to create graph adapter")
return nil, errors.Wrap(err, "failed to obtain workloadidentity credentials")
}
// Create authentication provider
authProvider, err := azauth.NewAzureIdentityAuthenticationProviderWithScopes(cred, MSGraphScopes)
if err != nil {
return nil, errors.Wrap(err, "failed to create auth provider")
}

// Initialize Microsoft Graph client
return msgraphsdk.NewGraphServiceClient(adapter), nil
return authProvider, nil
}

// graphQuery is a concrete implementation that interacts with Microsoft Graph API.
func (g *GraphQuery) graphQuery(ctx context.Context, azureCreds map[string]string, in *v1beta1.Input) (interface{}, error) {
identityType := v1beta1.IdentityTypeAzureServicePrincipalCredentials

if in.Identity != nil && in.Identity.Type != "" {
identityType = in.Identity.Type
}
// Create the Microsoft Graph client
client, err := g.createGraphClient(azureCreds)
client, err := g.createGraphClient(azureCreds, identityType)
if err != nil {
return nil, err
}
Expand Down
Loading
Loading