diff --git a/docs/getting-started/assets/replace-user-banner.png b/docs/getting-started/assets/replace-user-banner.png new file mode 100644 index 0000000..b68b9df Binary files /dev/null and b/docs/getting-started/assets/replace-user-banner.png differ diff --git a/docs/getting-started/assets/service-account-crossplane.png b/docs/getting-started/assets/service-account-crossplane.png new file mode 100644 index 0000000..f2c274b Binary files /dev/null and b/docs/getting-started/assets/service-account-crossplane.png differ diff --git a/docs/getting-started/assets/update-temporary-password.png b/docs/getting-started/assets/update-temporary-password.png new file mode 100644 index 0000000..9bd395c Binary files /dev/null and b/docs/getting-started/assets/update-temporary-password.png differ diff --git a/docs/getting-started/getting-started.md b/docs/getting-started/getting-started.md new file mode 100644 index 0000000..f1bc577 --- /dev/null +++ b/docs/getting-started/getting-started.md @@ -0,0 +1,154 @@ +# Getting started + +This guide will let you set up everything to try out crossplane-contrib/provider-keycloak on a fresh kind cluster. + +## Prerequisites + +[ctlptl](https://github.com/tilt-dev/ctlptl), [kind](https://kind.sigs.k8s.io/), [kubectl](https://kubernetes.io/docs/tasks/tools/#kubectl), [helm](https://helm.sh/docs/intro/install/) + +This example is written with linux in mind, but it will work on Windows with PowerShell 7 as well. + +## Keycloak up and running + +This is an express installation of keycloak on new kind cluster. + +``` sh +ctlptl apply -f kind-kustomize/cluster/cluster.yaml + +kubectl apply -f kind-kustomize/keycloak/keycloak.yaml +kubectl wait --for=condition=Available deployment kc -n keycloak --timeout=180s +kubectl port-forward -n keycloak svc/keycloak 8080:80 +``` + + +``` +❯ ctlptl apply -f kind-kustomize/cluster/cluster.yaml +No kind clusters found. +Creating cluster "provider-keycloak-cluster" ... + ✓ Ensuring node image (kindest/node:v1.31.0) đŸ–ŧ + ✓ Preparing nodes đŸ“Ļ đŸ“Ļ + ✓ Writing configuration 📜 + ✓ Starting control-plane 🕹ī¸ + ✓ Installing CNI 🔌 + ✓ Installing StorageClass 💾 + ✓ Joining worker nodes 🚜 +Set kubectl context to "kind-provider-keycloak-cluster" +You can now use your cluster with: + +kubectl cluster-info --context kind-provider-keycloak-cluster + +Have a question, bug, or feature request? Let us know! https://kind.sigs.k8s.io/#community 🙂 +Switched to context "kind-provider-keycloak-cluster". + 🔌 Connected cluster kind-provider-keycloak-cluster to registry ctlptl-registry at localhost:52145 + 👐 Push images to the cluster like 'docker push localhost:52145/alpine' +cluster.ctlptl.dev/kind-provider-keycloak-cluster created + +``` + +``` +> kubectl apply -f kind-kustomize/keycloak/keycloak.yaml +namespace/keycloak created +configmap/keycloak-cm created +service/keycloak created +deployment.apps/kc created + +``` + +``` +> kubectl wait --for=condition=Available deployment kc -n keycloak --timeout=180s +deployment.apps/kc condition met + +> kubectl port-forward -n keycloak svc/keycloak 8080:80 +Forwarding from 127.0.0.1:8080 -> 8080 +Forwarding from [::1]:8080 -> 8080 +Handling connection for 8080 +``` + +When surfing into the keycloak UI at http://localhost:8080 you can logon as admin/admin. You are then prompted to replace the temporary admin account with a permanent one. For the purpose of demonstrating or getting started with this crossplane provider you can skip this step. Make sure the new user can log on and has the correct access (typically the admin role) before deleting the temporary user. + +![An orange banner at the top urging the temporary user to be replaced](assets/replace-user-banner.png) + +Refer to the keycloak documentation on how to best harden security for your setup of keycloak and consider using an external database. https://www.keycloak.org/docs/latest/server_admin/#proc-creating-user_server_administration_guide + + +## Installing crossplane + +The procedure to install crossplane is described in better detail on the crossplane main repository, and on their webpage: https://docs.crossplane.io/latest/software/install/ + +Here is a minimal example to get up and running with everything you need. + +``` sh +helm repo add crossplane-stable https://charts.crossplane.io/stable +helm repo update +helm install crossplane --namespace crossplane-system --create-namespace crossplane-stable/crossplane + +``` + +The following step will bootstrap a working client in the master realm with the admin role that crossplane will use in a future step. You should consider learning to set up a similar client through the UI or through the API in a manner which fits your security practices. + +``` sh +# creates a config map with the script to run +kubectl create configmap client-script -n keycloak --from-file=kind-kustomize/crossplane/create-client.sh + +# creates a job to run the script from within kubernetes. +kubectl apply -f kind-kustomize/crossplane/create-client.yaml + +``` + +We can now create the keycloak crossplane provider and configure it to use the client withing the master realm to perform actions there. + +The settings for the client will also make it appear as a service-account user in the realm. + +![Displays the crossplane service-account user](assets/service-account-crossplane.png) + +``` sh +# deploys the keycloak provider +kubectl apply -f ./kind-kustomize/crossplane/provider.yaml +echo "Waiting for the required CRDs to show up..." +sleep 10 +kubectl wait --for=condition=established crd providerconfigs.keycloak.crossplane.io --timeout=15s + +kubectl apply -f ./kind-kustomize/crossplane/providerconfig.yaml +``` + +Finally we can try out using our keycloak crossplane provider, here is an example of creating a new realm. + +``` sh +kubectl apply -f ./kind-kustomize/test-realm/realm.yaml +``` + +If we want to observe the new realm to be able to use data generated inside it we can leverage existing functions for the provider. + +``` +kubectl apply -f ./kind-kustomize/crossplane/keycloak-built-in-objects/xrd.yaml +kubectl apply -f ./kind-kustomize/crossplane/keycloak-built-in-objects/composition.yaml +kubectl apply -f ./kind-kustomize/crossplane/keycloak-built-in-objects/functions.yaml +# written specifically for the test-realm +kubectl apply -f ./kind-kustomize/crossplane/keycloak-built-in-objects/xr-test-realm.yaml +``` + +Once synced the observable default items will all be available through kube-api. + +``` sh +kubectl get roles.role.keycloak.crossplane.io +``` + +This will then allow us to reference them in crossplane like in the example below that creates a user in the new role and assigns them the administrative role. The format is `builtin---`. Thus for role *realm-admin* in the realm *test-realm* which is a client role for the client *realm-management* the name would be `builtin-test-realm-realm-management-realm-admin` :) + +``` sh +kubectl apply -f ./kind-kustomize/test-realm/admin-user.yaml +``` + +Once this has synched, you can surf to the security admin console of the test-realm, sign in with testadmin/testadmin and you will be prompted to update your temporary password for the user testadmin. + +http://localhost:8080/admin/test-realm/console/ + +![update-temporary-password](assets/update-temporary-password.png) + +## clean up + +To delete the cluster you created with ctlptl, run the following. + +``` sh +ctlptl delete -f kind-kustomize/cluster/cluster.yaml +``` \ No newline at end of file diff --git a/docs/getting-started/kind-kustomize/cluster/cluster.yaml b/docs/getting-started/kind-kustomize/cluster/cluster.yaml new file mode 100644 index 0000000..e391d33 --- /dev/null +++ b/docs/getting-started/kind-kustomize/cluster/cluster.yaml @@ -0,0 +1,9 @@ +apiVersion: ctlptl.dev/v1alpha1 +kind: Cluster +product: kind +registry: ctlptl-registry +kindV1Alpha4Cluster: + name: provider-keycloak-cluster + nodes: + - role: worker + - role: control-plane \ No newline at end of file diff --git a/docs/getting-started/kind-kustomize/crossplane/create-client.sh b/docs/getting-started/kind-kustomize/crossplane/create-client.sh new file mode 100644 index 0000000..ff2b24c --- /dev/null +++ b/docs/getting-started/kind-kustomize/crossplane/create-client.sh @@ -0,0 +1,45 @@ +# uses curl to invoke the keycloak REST api for crossplane +# gets a token for the master realm +echo "Logging on as admin in keycloak to create the crossplane client and grant it the admin role in the master realm" +mastertoken=$(curl -k -g -d "client_id=admin-cli" -d "username=admin" -d "password=admin" -d "grant_type=password" -d "client_secret=" "http://keycloak.keycloak:80/realms/master/protocol/openid-connect/token" | sed 's/.*access_token":"//g' | sed 's/".*//g'); +# echo $mastertoken; + +id="9d2308c3-8972-40cf-9cca-1256745c16d4"; +url="http://keycloak.keycloak:80/admin/realms/master"; +clienturl="$url/clients/$id"; + +# creates a new client named "crossplane" +curl -X POST -k -g "$url/clients" \ +-H "Authorization: Bearer $mastertoken" \ +-H "Content-Type: application/json" \ +--data-raw ' +{ + "id":"'$id'", + "name":"crossplane", + "clientId":"crossplane", + "secret":"xppw_OJKzQjuBoyPlIEePgiWg", + "clientAuthenticatorType":"client-secret", + "serviceAccountsEnabled":"true", + "standardFlowEnabled":"false" +}' + +# GETs the service-account-user for the client - GET $url/clients/{id}/service-account-user +userid=$(curl -X GET -k -g "$clienturl/service-account-user" -H "Authorization: Bearer $mastertoken" | sed 's/.*id":"//g' | sed 's/".*//g') + +# lists available realm roles +# GET /{realm}/users/{id}/role-mappings/realm/available +roles=$(curl -X GET -k -g -H "Authorization: Bearer $mastertoken" "$url/roles") + +# gets the id of the admin role +admin_id=$(echo $roles | jq -r '.[] | select(.name == "admin") | .id') + +# adds service account role admin to the client's user +curl -X POST -k -g "$url/users/$userid/role-mappings/realm/" \ +-H "Authorization: Bearer $mastertoken" \ +-H "Content-Type: application/json" \ +--data-raw '[ +{ + "id":"'$admin_id'", + "name":"admin" +} +]' \ No newline at end of file diff --git a/docs/getting-started/kind-kustomize/crossplane/create-client.yaml b/docs/getting-started/kind-kustomize/crossplane/create-client.yaml new file mode 100644 index 0000000..2e9be63 --- /dev/null +++ b/docs/getting-started/kind-kustomize/crossplane/create-client.yaml @@ -0,0 +1,59 @@ +# The pod for this job should mount a volume that will be created from a config map containing a file with a bash script that it runs +apiVersion: batch/v1 +kind: Job +metadata: + name: create-client-crossplane + namespace: keycloak +spec: + backoffLimit: 1 + template: + spec: + restartPolicy: Never + initContainers: + - command: + - sh + - -c + - | + set -x; + echo "Waiting for master realm to become ready..." + while [ $(curl -sw '%{http_code}' "http://keycloak.keycloak/realms/master" -o /dev/null) -ne 200 ]; do + sleep 15; + done; + + echo "$SVC_HOST:$SVC_PORT connection OK ✓" + image: dwdraju/alpine-curl-jq + imagePullPolicy: IfNotPresent + name: svcchecker + resources: + limits: + cpu: 20m + memory: 32Mi + requests: + cpu: 20m + memory: 32Mi + securityContext: + allowPrivilegeEscalation: false + runAsGroup: 1000 + runAsNonRoot: true + runAsUser: 1000 + terminationMessagePath: /dev/termination-log + terminationMessagePolicy: File + containers: + - name: bash + image: dwdraju/alpine-curl-jq + command: ["bash"] + args: ["/opt/script/create-client.sh"] + volumeMounts: + - name: client-script + mountPath: /opt/script/ + resources: + limits: + cpu: 100m + memory: 32Mi + requests: + cpu: 100m + memory: 32Mi + volumes: + - name: client-script + configMap: + name: client-script diff --git a/docs/getting-started/kind-kustomize/crossplane/keycloak-built-in-objects/composition.yaml b/docs/getting-started/kind-kustomize/crossplane/keycloak-built-in-objects/composition.yaml new file mode 100644 index 0000000..338fc04 --- /dev/null +++ b/docs/getting-started/kind-kustomize/crossplane/keycloak-built-in-objects/composition.yaml @@ -0,0 +1,46 @@ +apiVersion: apiextensions.crossplane.io/v1 +kind: Composition +metadata: + name: keycloak-builtin-objects +spec: + compositeTypeRef: + apiVersion: keycloak.crossplane.io/v1alpha1 + kind: XBuiltinObjects + mode: Pipeline + pipeline: + - step: pull-provider-configs + functionRef: + name: function-extra-resources + input: + apiVersion: extra-resources.fn.crossplane.io/v1beta1 + kind: Input + spec: + extraResources: + - kind: Secret + into: secrets + apiVersion: v1 + type: Selector + selector: + minMatch: 1 + maxMatch: 100 + matchLabels: + - key: type + type: Value + value: provider-credentials + - kind: Role + into: roles + apiVersion: role.keycloak.crossplane.io/v1alpha1 + type: Selector + selector: + minMatch: 0 + maxMatch: 20 + matchLabels: + - key: type + type: Value + value: modifiedrole + - step: keycloak-builtin-objects + functionRef: + name: function-keycloak-builtin-objects + - step: automatically-detect-ready-composed-resources + functionRef: + name: function-auto-ready \ No newline at end of file diff --git a/docs/getting-started/kind-kustomize/crossplane/keycloak-built-in-objects/functions.yaml b/docs/getting-started/kind-kustomize/crossplane/keycloak-built-in-objects/functions.yaml new file mode 100644 index 0000000..e2bc3f4 --- /dev/null +++ b/docs/getting-started/kind-kustomize/crossplane/keycloak-built-in-objects/functions.yaml @@ -0,0 +1,34 @@ +--- +apiVersion: pkg.crossplane.io/v1beta1 +kind: Function +metadata: + name: function-extra-resources + annotations: + # This tells crossplane beta render to connect to the function locally. + #render.crossplane.io/runtime: Development +spec: + # This is ignored when using the Development runtime. + package: xpkg.upbound.io/crossplane-contrib/function-extra-resources:v0.0.3 +--- +apiVersion: pkg.crossplane.io/v1beta1 +kind: Function +metadata: + name: function-auto-ready + annotations: + # This tells crossplane beta render to connect to the function locally. + #render.crossplane.io/runtime: Development +spec: + # This is ignored when using the Development runtime. + package: xpkg.upbound.io/crossplane-contrib/function-auto-ready:v0.2.1 +--- +apiVersion: pkg.crossplane.io/v1beta1 +kind: Function +metadata: + name: function-keycloak-builtin-objects + #annotations: + # # This tells crossplane beta render to connect to the function locally. + # render.crossplane.io/runtime: Development +spec: + # This is ignored when using the Development runtime. + package: registry.gitlab.com/corewire/images/crossplane/function-keycloak-builtin-objects:v1.0.0 + packagePullPolicy: Always diff --git a/docs/getting-started/kind-kustomize/crossplane/keycloak-built-in-objects/xr-test-realm.yaml b/docs/getting-started/kind-kustomize/crossplane/keycloak-built-in-objects/xr-test-realm.yaml new file mode 100644 index 0000000..779c802 --- /dev/null +++ b/docs/getting-started/kind-kustomize/crossplane/keycloak-built-in-objects/xr-test-realm.yaml @@ -0,0 +1,20 @@ +--- +# Example for a custom realm (custom realms have different builtin clients/roles than the master realm) +apiVersion: keycloak.crossplane.io/v1alpha1 +kind: XBuiltinObjects +metadata: + name: keycloak-builtin-objects-dev +spec: + providerConfigName: keycloak-config + providerSecretName: keycloak-credentials + realm: test-realm + builtinClients: + - account + - account-console + - admin-cli + - broker + - realm-management + - security-admin-console + builtinRealmRoles: + - offline_access + - uma_authorization \ No newline at end of file diff --git a/docs/getting-started/kind-kustomize/crossplane/keycloak-built-in-objects/xrd.yaml b/docs/getting-started/kind-kustomize/crossplane/keycloak-built-in-objects/xrd.yaml new file mode 100644 index 0000000..5930757 --- /dev/null +++ b/docs/getting-started/kind-kustomize/crossplane/keycloak-built-in-objects/xrd.yaml @@ -0,0 +1,57 @@ +apiVersion: apiextensions.crossplane.io/v1 +kind: CompositeResourceDefinition +metadata: + name: xbuiltinobjects.keycloak.crossplane.io +spec: + group: keycloak.crossplane.io + names: + kind: XBuiltinObjects + plural: xbuiltinobjects + versions: + - name: v1alpha1 + served: true + referenceable: true + schema: + openAPIV3Schema: + type: object + properties: + spec: + type: object + properties: + realm: + type: string + description: Realm to import the builtin clients/roles from + builtinClients: + type: array + items: + type: string + description: List of clients to import from the realm + builtinRealmRoles: + type: array + items: + type: string + enum: + - offline_access + - uma_authorization + - admin + - create-realm + - default-roles-master + description: List of realm roles to import from the realm + builtinAuthenticationFlows: + type: array + items: + type: string + description: List of authentication flows to import from the realm + providerConfigName: + type: string + description: Name of the provider config to attach to the imported clients/roles + providerSecretName: + type: string + description: Name of the secret containing the provider credentials (Secret must have a label with key=type and value=provider-credentials to be found) + required: + - providerConfigName + - providerSecretName + - realm + required: + - spec + diff --git a/docs/getting-started/kind-kustomize/crossplane/provider.yaml b/docs/getting-started/kind-kustomize/crossplane/provider.yaml new file mode 100644 index 0000000..5941dae --- /dev/null +++ b/docs/getting-started/kind-kustomize/crossplane/provider.yaml @@ -0,0 +1,6 @@ +apiVersion: pkg.crossplane.io/v1 +kind: Provider +metadata: + name: provider-keycloak +spec: + package: xpkg.upbound.io/crossplane-contrib/provider-keycloak:v1.8.0 diff --git a/docs/getting-started/kind-kustomize/crossplane/providerconfig.yaml b/docs/getting-started/kind-kustomize/crossplane/providerconfig.yaml new file mode 100644 index 0000000..e2c2cc2 --- /dev/null +++ b/docs/getting-started/kind-kustomize/crossplane/providerconfig.yaml @@ -0,0 +1,29 @@ +apiVersion: v1 +kind: Secret +metadata: + name: keycloak-credentials + namespace: crossplane-system + labels: + type: provider-credentials +type: Opaque +stringData: # these have to be in a credentials json when used with the xrd XBuiltinObjects + credentials: | + { + "client_id": "crossplane", + "client_secret": "xppw_OJKzQjuBoyPlIEePgiWg", + "url": "http://keycloak.keycloak.svc.cluster.local", + "realm": "master" + } +--- +apiVersion: keycloak.crossplane.io/v1beta1 +kind: ProviderConfig +metadata: + name: keycloak-config + namespace: crossplane-system +spec: + credentials: + source: Secret + secretRef: + name: keycloak-credentials + key: credentials + namespace: crossplane-system \ No newline at end of file diff --git a/docs/getting-started/kind-kustomize/keycloak/keycloak.yaml b/docs/getting-started/kind-kustomize/keycloak/keycloak.yaml new file mode 100644 index 0000000..993412a --- /dev/null +++ b/docs/getting-started/kind-kustomize/keycloak/keycloak.yaml @@ -0,0 +1,76 @@ +apiVersion: v1 +kind: Namespace +metadata: + name: keycloak +--- +apiVersion: v1 +data: + KC_BOOTSTRAP_ADMIN_PASSWORD: admin + KC_BOOTSTRAP_ADMIN_USERNAME: admin + KC_HOSTNAME: http://localhost:8080 + KC_LOG_LEVEL: info +kind: ConfigMap +metadata: + labels: + app: keycloak + name: keycloak-cm + namespace: keycloak +--- +apiVersion: v1 +kind: Service +metadata: + name: keycloak + namespace: keycloak +spec: + ports: + - name: http + port: 80 + targetPort: 8080 + selector: + app: keycloak + type: ClusterIP +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + annotations: + await: service + labels: + app: keycloak + name: kc + namespace: keycloak +spec: + replicas: 1 + selector: + matchLabels: + app: keycloak + template: + metadata: + labels: + app: keycloak + app.kubernetes.io/name: keycloak + spec: + containers: + - args: + - start-dev + envFrom: + - configMapRef: + name: keycloak-cm + image: quay.io/keycloak/keycloak:26.0.2 + name: keycloak + ports: + - containerPort: 8080 + resources: + limits: + cpu: 1000m + memory: 2Gi + requests: + cpu: 250m + memory: 500Mi + readinessProbe: + httpGet: + path: /realms/master + port: 8080 + initialDelaySeconds: 30 + periodSeconds: 10 + failureThreshold: 6 diff --git a/docs/getting-started/kind-kustomize/test-realm/admin-user.yaml b/docs/getting-started/kind-kustomize/test-realm/admin-user.yaml new file mode 100644 index 0000000..4bb3f43 --- /dev/null +++ b/docs/getting-started/kind-kustomize/test-realm/admin-user.yaml @@ -0,0 +1,47 @@ +apiVersion: user.keycloak.crossplane.io/v1alpha1 +kind: User +metadata: + name: testadmin + namespace: keycloak +spec: + forProvider: + realmId: "test-realm" + username: "testadmin" + firstName: "Test" + lastName: "Admin" + email: "testadmin@keycloak.keycloak" + initialPassword: + - temporary: true + valueSecretRef: + namespace: "keycloak" + name: "testadmin-temp-credentials" + key: "password" + providerConfigRef: + name: "keycloak-config" +--- +apiVersion: v1 +kind: Secret +metadata: + name: testadmin-temp-credentials + namespace: keycloak + labels: + type: user-temporary-credentials +type: Opaque +stringData: + password: "testadmin" +--- +apiVersion: user.keycloak.crossplane.io/v1alpha1 +kind: Roles +metadata: + name: test-realm-testadmin + namespace: keycloak +spec: + forProvider: + realmId: "test-realm" + roleIdsRefs: + - name: builtin-test-realm-realm-management-realm-admin + - name: builtin-test-realm-account-manage-account + userIdRef: + name: testadmin + providerConfigRef: + name: "keycloak-config" diff --git a/docs/getting-started/kind-kustomize/test-realm/realm.yaml b/docs/getting-started/kind-kustomize/test-realm/realm.yaml new file mode 100644 index 0000000..2ba1376 --- /dev/null +++ b/docs/getting-started/kind-kustomize/test-realm/realm.yaml @@ -0,0 +1,10 @@ +apiVersion: realm.keycloak.crossplane.io/v1alpha1 +kind: Realm +metadata: + name: test-realm + namespace: keycloak +spec: + forProvider: + realm: "test-realm" + providerConfigRef: + name: "keycloak-config" \ No newline at end of file