Skip to content

Commit

Permalink
EntraUser provisioning resource (#91)
Browse files Browse the repository at this point in the history
* EntraUser provisioning resource

* Fix user creation bug in pulumi provisioner

* Add ObjectMeta to EntraUser in test file
  • Loading branch information
fraliv13 committed Feb 9, 2024
1 parent 2249f0c commit d78c9d9
Show file tree
Hide file tree
Showing 25 changed files with 1,515 additions and 2 deletions.
42 changes: 42 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -326,6 +326,48 @@ spec:
workspaceFriendlyName: Charisma
```

### EntraUser

`EntraUser` is a Custom Resource Definition (CRD) that represents a user for Entra Id.

Definition can be found [here](./helm/crds/provisioning.totalsoft.ro_entrausers.yaml)

## Spec

The `EntraUser` spec has the following fields:

- `userPrincipalName`: The user principal name of the user. This is typically the user's email address or username.
- `displayName`: The display name of the user.
- `initialPassword`: The initial password for the user. If this is not provided, a random password will be generated.
- `domainRef`: The reference to the domain that the user belongs to.
- `platformRef`: The reference to the platform that the user belongs to.

## Example

Here's an example of an `EntraUser` resource:

```yaml
apiVersion: provisioning.totalsoft.ro/v1alpha1
kind: EntraUser
metadata:
name: example-user
namespace: qa-lsng
spec:
userPrincipalName: "[email protected]"
displayName: "Example User"
initialPassword: "password123"
domainRef: "entra-users"
platformRef: "qa"
exports:
- domain: entra-users
initialPassword:
toVault:
keyTemplate: InitialPassword
userPrincipalName:
toVault:
keyTemplate: UserPrincipalName
```

## configuration.totalsoft.ro
manages external configuration for the services in the platform, read more about from the [Twelve-Factor App ](https://12factor.net/config) methodology.

Expand Down
191 changes: 191 additions & 0 deletions helm/crds/provisioning.totalsoft.ro_entrausers.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
---
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
annotations:
controller-gen.kubebuilder.io/version: v0.14.0
name: entrausers.provisioning.totalsoft.ro
spec:
group: provisioning.totalsoft.ro
names:
kind: EntraUser
listKind: EntraUserList
plural: entrausers
singular: entrauser
scope: Namespaced
versions:
- additionalPrinterColumns:
- jsonPath: .spec.displayName
name: Display name
type: string
- jsonPath: .spec.userPrincipalName
name: User principal name
type: string
- jsonPath: .spec.platformRef
name: Platform
type: string
- jsonPath: .spec.domainRef
name: Domain
type: string
name: v1alpha1
schema:
openAPIV3Schema:
properties:
apiVersion:
description: |-
APIVersion defines the versioned schema of this representation of an object.
Servers should convert recognized schemas to the latest internal value, and
may reject unrecognized values.
More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources
type: string
kind:
description: |-
Kind is a string value representing the REST resource this object represents.
Servers may infer this from the endpoint the client submits requests to.
Cannot be updated.
In CamelCase.
More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds
type: string
metadata:
type: object
spec:
properties:
dependsOn:
description: List of dependencies
items:
properties:
kind:
description: Kind is a string value representing the REST resource
this dependency represents.
type: string
name:
description: ' The name of the dependency.'
type: string
required:
- kind
- name
type: object
type: array
displayName:
description: DisplayName represents the display name of the user,
e.g. "John Doe"
type: string
domainRef:
description: Business Domain that this resource is provision for.
type: string
exports:
description: Export provisioning values spec.
items:
properties:
domain:
description: The domain or bounded-context in which this database
will be used.
type: string
initialPassword:
description: The initial password for the user
properties:
toConfigMap:
properties:
keyTemplate:
type: string
required:
- keyTemplate
type: object
toVault:
properties:
keyTemplate:
type: string
required:
- keyTemplate
type: object
type: object
userPrincipalName:
description: The user principal name
properties:
toConfigMap:
properties:
keyTemplate:
type: string
required:
- keyTemplate
type: object
toVault:
properties:
keyTemplate:
type: string
required:
- keyTemplate
type: object
type: object
required:
- domain
type: object
type: array
initialPassword:
description: InitialPassword represents the initial password for the
user
type: string
platformRef:
description: Target platform (custom resource name).
type: string
target:
default:
category: Tenant
description: The provisioning target.
properties:
category:
default: Tenant
description: 'Provisioning target type. Possible values: Tenant,
Platform'
enum:
- Tenant
- Platform
type: string
filter:
description: |-
Filter targets (applies for category "Tenant").
If ommited all targets are selected.
properties:
kind:
default: Blacklist
description: 'Includes or excludes the speciffied targets.
Possibile values: Blacklist, Whitelist'
enum:
- Blacklist
- Whitelist
type: string
values:
description: A list of targets to include or exculde
items:
type: string
type: array
required:
- kind
type: object
required:
- category
type: object
tenantOverrides:
additionalProperties:
x-kubernetes-preserve-unknown-fields: true
description: |-
Overrides for tenants. Dictionary with tenant name as key, spec override as value.
The spec override has the same structure as Spec
type: object
userPrincipalName:
description: UserPrincipalName represents the user principal name,
e.g. "[email protected]"
type: string
required:
- displayName
- domainRef
- platformRef
- target
- userPrincipalName
type: object
required:
- spec
type: object
served: true
storage: true
subresources: {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
package pulumi

import (
"fmt"

"github.com/pulumi/pulumi-azuread/sdk/v5/go/azuread"
"github.com/pulumi/pulumi-random/sdk/v4/go/random"
"github.com/pulumi/pulumi/sdk/v3/go/pulumi"
"totalsoft.ro/platform-controllers/internal/controllers/provisioning"
provisioningv1 "totalsoft.ro/platform-controllers/pkg/apis/provisioning/v1alpha1"
)

func deployEntraUser(target provisioning.ProvisioningTarget,
entraUser *provisioningv1.EntraUser,
dependencies []pulumi.Resource,
ctx *pulumi.Context) (*azuread.User, error) {

valueExporter := handleValueExport(target)
gvk := provisioningv1.SchemeGroupVersion.WithKind("AzureDatabase")

initialPassword := pulumi.String(entraUser.Spec.InitialPassword).ToStringOutput()
if entraUser.Spec.InitialPassword == "" {
randomPassword, err := random.NewRandomPassword(ctx, fmt.Sprintf("%s-initial-password", entraUser.Spec.UserPrincipalName), &random.RandomPasswordArgs{
Length: pulumi.Int(10),
Upper: pulumi.Bool(true),
MinUpper: pulumi.Int(1),
Lower: pulumi.Bool(true),
MinLower: pulumi.Int(1),
Numeric: pulumi.Bool(true),
MinNumeric: pulumi.Int(1),
Special: pulumi.Bool(true),
MinSpecial: pulumi.Int(1),
})

if err != nil {
return nil, err
}

initialPassword = randomPassword.Result
}

user, err := azuread.NewUser(ctx, entraUser.Name, &azuread.UserArgs{
UserPrincipalName: pulumi.String(entraUser.Spec.UserPrincipalName),
DisplayName: pulumi.String(entraUser.Spec.DisplayName),
Password: initialPassword,
})
if err != nil {
return nil, err
}

for _, exp := range entraUser.Spec.Exports {
domain := exp.Domain
if domain == "" {
domain = entraUser.Spec.DomainRef
}

err = valueExporter(newExportContext(ctx, domain, entraUser.Name, entraUser.ObjectMeta, gvk),
map[string]exportTemplateWithValue{
"initialPassword": {exp.InitialPassword, initialPassword},
"userPrincipalName": {exp.UserPrincipalName, pulumi.String(entraUser.Spec.UserPrincipalName)},
})
if err != nil {
return nil, err
}
}
return user, nil
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package pulumi

import (
"testing"

"github.com/pulumi/pulumi/sdk/v3/go/pulumi"
"github.com/stretchr/testify/assert"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
provisioningv1 "totalsoft.ro/platform-controllers/pkg/apis/provisioning/v1alpha1"
)

func TestDeployEntraUser(t *testing.T) {
t.Run("maximal entra user spec", func(t *testing.T) {
platform := "dev"
tenant := newTenant("tenant1", platform)
entraUser := &provisioningv1.EntraUser{
ObjectMeta: metav1.ObjectMeta{
Name: "my-entra-user",
},
Spec: provisioningv1.EntraUserSpec{
UserPrincipalName: "[email protected]",
DisplayName: "Example User",
InitialPassword: "password123",
ProvisioningMeta: provisioningv1.ProvisioningMeta{
DomainRef: "example-domain",
},
},
}

err := pulumi.RunErr(func(ctx *pulumi.Context) error {
user, err := deployEntraUser(tenant, entraUser, []pulumi.Resource{}, ctx)
assert.NoError(t, err)
assert.NotNil(t, user)
return nil

}, pulumi.WithMocks("project", "stack", mocks(0)))
assert.NoError(t, err)
})
}
12 changes: 11 additions & 1 deletion internal/controllers/provisioning/provisioners/pulumi/pulumi.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,9 @@ func Create(target provisioning.ProvisioningTarget, domain string, infra *provis
anyHelmRelease := len(infra.HelmReleases) > 0
anyVirtualMachine := len(infra.AzureVirtualMachines) > 0
anyVirtualDesktop := len(infra.AzureVirtualDesktops) > 0
anyEntraUser := len(infra.EntraUsers) > 0

anyResource := anyAzureDb || anyManagedAzureDb || anyHelmRelease || anyVirtualMachine || anyVirtualDesktop
anyResource := anyAzureDb || anyManagedAzureDb || anyHelmRelease || anyVirtualMachine || anyVirtualDesktop || anyEntraUser
needsResourceGroup := anyVirtualMachine || anyVirtualDesktop

stackName := provisioning.Match(target,
Expand Down Expand Up @@ -222,6 +223,8 @@ func deployResource(target provisioning.ProvisioningTarget,
}

switch kind {
case string(provisioningv1.ProvisioningResourceKindEntraUser):
return deployEntraUser(target, res.(*provisioningv1.EntraUser), dependencies, ctx)
case string(provisioningv1.ProvisioningResourceKindAzureDatabase):
return deployAzureDb(target, res.(*provisioningv1.AzureDatabase), dependencies, ctx)
case string(provisioningv1.ProvisioningResourceKindAzureManagedDatabase):
Expand Down Expand Up @@ -284,6 +287,13 @@ func deployFunc(target provisioning.ProvisioningTarget, domain string,
rgName = &resGroupName
}

for _, user := range infra.EntraUsers {
_, err := deployResourceWithDeps(target, rgName, user, provisionedRes, infra, ctx)
if err != nil {
return err
}
}

for _, db := range infra.AzureDbs {
_, err := deployResourceWithDeps(target, rgName, db, provisionedRes, infra, ctx)
if err != nil {
Expand Down
Loading

0 comments on commit d78c9d9

Please sign in to comment.