Skip to content

Commit

Permalink
Merge pull request #236 from canonical/IAM-767
Browse files Browse the repository at this point in the history
IAM 767: openfga dev setup
  • Loading branch information
wood-push-melon authored Mar 21, 2024
2 parents 9099b37 + dc6389e commit 588680c
Show file tree
Hide file tree
Showing 14 changed files with 190 additions and 107 deletions.
2 changes: 2 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@ npm-build:
.PHONY: npm-build

dev:
@echo "after job admin-ui-openfga-setup has run, restart the identity-platform-admin-ui deployment"
@echo "to make sure the changes in the configmap are picked up"
@$(MICROK8S_REGISTRY_FLAG) $(SKAFFOLD) run \
--mute-logs=all \
--port-forward \
Expand Down
32 changes: 31 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,12 @@ This is the Admin UI for the Canonical Identity Platform.
- `IDP_CONFIGMAP_NAMESPACE`: namespace of the k8s config map containing Identity Providers
- `SCHEMAS_CONFIGMAP_NAME`: name of the k8s config map containing Identity Schemas
- `SCHEMAS_CONFIGMAP_NAMESPACE`: namespace of the k8s config map containing Identity Schemas

- `OPENFGA_API_SCHEME`: scheme for the OpenFGA host variable, either `http` or `https`
- `OPENFGA_API_HOST`: host of the OpenFGA server
- `OPENFGA_API_TOKEN`: token used to interact with OpenFGA server, dictated by OpenFGA server
- `OPENFGA_STORE_ID`: ID of the OpenFGA store the application will talk to
- `OPENFGA_AUTHORIZATION_MODEL_ID`: ID of the OpenFGA authorization model the application will talk to
- `AUTHORIZATION_ENABLED`: flag defining if the OpenFGA authorization middleware is enabled and, for the time being, if any of the RBAC API are using OpenFGA (to be fixed by https://github.com/canonical/identity-platform-admin-ui/issues/221), default to `false`

## Development setup

Expand Down Expand Up @@ -56,6 +61,31 @@ As a requirement, please make sure to:

Run `make dev` to get a working environment in k8s

### OpenFGA initialization

The Admin Service comes up with authorization disabled (see `AUTHORIZATION_ENABLED` env var), The env vars `OPENFGA_AUTHORIZATION_MODEL_ID` and `OPENFGA_STORE_ID` which are needed for the correct functioning of the RBAC APIs get set by the job `admin-ui-openfga-setup`, after this has completed a developer is supposed to bounce the deployment to get the application to source the new env vars, setting `AUTHORIZATION_ENABLED` will make sure those endpoints use OpenFGA as a backend instead of a `NoOp implementation` (behaviour will change, see https://github.com/canonical/identity-platform-admin-ui/issues/221)

```
# Wait for the openfga setup job to complete
kubectl wait --for=condition=complete job/admin-ui-openfga-setup
# Edit the configmap to enable authorization by setting AUTHORIZATION_ENABLED=true
kubectl edit configmap identity-platform-admin-ui
# Restart the admin UI apply the changes
kubectl rollout restart deployment identity-platform-admin-ui
```


K8s jobs don't get deleted on their own so if you wish to make changes to the openfga model, you need to make sure that the job for setting up openfga is deleted before redeploying the the admin UI:

```
kubectl delete job admin-ui-openfga-setup
make dev
```

ensure environment variables in the `identity-platform-admin-ui` configmap reflect the status you want


## Endpoint examples

Expand Down
18 changes: 1 addition & 17 deletions build.sh
Original file line number Diff line number Diff line change
Expand Up @@ -8,25 +8,9 @@

set -e

rockcraft="rockcraft.yaml"
rockcraft_backup="rockcraft_bak.yaml"

restore() {
mv "$rockcraft_backup" "$rockcraft"
rm -f "$rockcraft_backup"
}
trap 'restore' INT TERM EXIT

# The ROCK image needs certain utilities to
# - create OpenFGA store and authorization model
# - export OpenFGA store ID and authorization model ID to Admin UI service
cp "$rockcraft" "$rockcraft_backup"
yq -i \
'.base = "[email protected]", .parts |= ({"utils": {"plugin": "nil", "stage-packages": ["curl", "jq"]}} + .)' \
"$rockcraft"
rockcraft pack -v

sudo skopeo --insecure-policy \
skopeo --insecure-policy \
copy "oci-archive:identity-platform-admin-ui_$(yq -r '.version' rockcraft.yaml)_amd64.rock" \
docker-daemon:"$IMAGE"

Expand Down
89 changes: 80 additions & 9 deletions cmd/createFgaModel.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,16 @@ import (
"context"
"fmt"
"net/url"
"strings"

"github.com/openfga/go-sdk/client"
"github.com/spf13/cobra"

metaV1 "k8s.io/apimachinery/pkg/apis/meta/v1"

"github.com/canonical/identity-platform-admin-ui/internal/authorization"
k8s "github.com/canonical/identity-platform-admin-ui/internal/k8s"

"github.com/canonical/identity-platform-admin-ui/internal/logging"
"github.com/canonical/identity-platform-admin-ui/internal/monitoring"
"github.com/canonical/identity-platform-admin-ui/internal/openfga"
Expand All @@ -27,7 +32,10 @@ var createFgaModelCmd = &cobra.Command{
apiUrl, _ := cmd.Flags().GetString("fga-api-url")
apiToken, _ := cmd.Flags().GetString("fga-api-token")
storeId, _ := cmd.Flags().GetString("fga-store-id")
createModel(apiUrl, apiToken, storeId)
kubeconfig, _ := cmd.Flags().GetString("kubeconfig")
k8sConfigMap, _ := cmd.Flags().GetString("store-k8s-configmap-resource")

createModel(apiUrl, apiToken, storeId, kubeconfig, k8sConfigMap)
},
}

Expand All @@ -36,32 +44,95 @@ func init() {

createFgaModelCmd.Flags().String("fga-api-url", "", "The openfga API URL")
createFgaModelCmd.Flags().String("fga-api-token", "", "The openfga API token")
createFgaModelCmd.Flags().String("fga-store-id", "", "The openfga store to create the model in")
createFgaModelCmd.Flags().String("fga-store-id", "", "The openfga store to create the model in, if empty one will be created")
createFgaModelCmd.Flags().String("kubeconfig", "", "The path to the kubeconfig file")
createFgaModelCmd.Flags().String("store-k8s-configmap-resource", "", "K8s configmap to store created data, format with namespace/configmap-name")
createFgaModelCmd.MarkFlagRequired("fga-api-url")
createFgaModelCmd.MarkFlagRequired("fga-api-token")
createFgaModelCmd.MarkFlagRequired("fga-store-id")
}

func createModel(apiUrl, apiToken, storeId string) {
func createModel(apiUrl, apiToken, storeId, kubeconfig, k8sConfigMap string) {
ctx := context.Background()

logger := logging.NewNoopLogger()
tracer := tracing.NewNoopTracer()
monitor := monitoring.NewNoopMonitor("", logger)

scheme, host, err := parseURL(apiUrl)
if err != nil {
panic(err)
}

cfg := openfga.NewConfig(scheme, host, storeId, apiToken, "", false, tracer, monitor, logger)

fgaClient := openfga.NewClient(cfg)
authModelReq := client.ClientWriteAuthorizationModelRequest{
TypeDefinitions: authorization.AuthModel.TypeDefinitions,
SchemaVersion: authorization.AuthModel.SchemaVersion,
Conditions: authorization.AuthModel.Conditions,

if storeId == "" {
storeId, err = fgaClient.CreateStore(ctx, "identity-admin-ui")

if err != nil {
panic(err)
}

fgaClient.SetStoreID(ctx, storeId)
}
modelId, err := fgaClient.WriteModel(context.Background(), &authModelReq)

modelId, err := fgaClient.WriteModel(
context.Background(),
&client.ClientWriteAuthorizationModelRequest{
TypeDefinitions: authorization.AuthModel.TypeDefinitions,
SchemaVersion: authorization.AuthModel.SchemaVersion,
Conditions: authorization.AuthModel.Conditions,
},
)

if err != nil {
panic(err)
}

fmt.Printf("Created model: %s\n", modelId)

if err := storeValuesK8sConfigMap(ctx, kubeconfig, k8sConfigMap, storeId, modelId); err != nil {
panic(err)
}
}

func storeValuesK8sConfigMap(ctx context.Context, kubeconfig, configMap, storeID, modelID string) error {
fmt.Println(kubeconfig, configMap, storeID, modelID)

if configMap == "" {
return nil
}

k8sClient, err := k8s.NewCoreV1Client(kubeconfig)

if err != nil {
return err
}

parts := strings.Split(configMap, "/")

if len(parts) != 2 {
return fmt.Errorf("invalid format for configmap resource %s: expected namespace/name", configMap)
}
cmNamespace, cmName := parts[0], parts[1]

cm, err := k8sClient.ConfigMaps(cmNamespace).Get(ctx, cmName, metaV1.GetOptions{})

if err != nil {
return err
}

cm.Data["OPENFGA_STORE_ID"] = storeID
cm.Data["OPENFGA_AUTHORIZATION_MODEL_ID"] = modelID

if _, err = k8sClient.ConfigMaps(cmNamespace).Update(ctx, cm, metaV1.UpdateOptions{}); err != nil {
return err
}

fmt.Printf("Configmap updated successfully: %s\n", configMap)

return nil
}

func parseURL(s string) (string, string, error) {
Expand Down
2 changes: 1 addition & 1 deletion cmd/serve.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ func serve() {
kPublicClient := ik.NewClient(specs.KratosPublicURL, specs.Debug)
oPublicClient := io.NewClient(specs.OathkeeperPublicURL, specs.Debug)

k8sCoreV1, err := k8s.NewCoreV1Client()
k8sCoreV1, err := k8s.NewCoreV1Client("")

if err != nil {
panic(err)
Expand Down
34 changes: 4 additions & 30 deletions deployments/kubectl/configMap.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ data:
RULES_CONFIGMAP_NAME: oathkeeper-rules
RULES_CONFIGMAP_FILE_NAME: access-rules.json
RULES_CONFIGMAP_NAMESPACE: default
OPENFGA_API_SCHEME: http
OPENFGA_API_HOST: openfga.default.svc.cluster.local:8080
OPENFGA_API_TOKEN: "42"
AUTHORIZATION_ENABLED: "false"

---
apiVersion: v1
Expand Down Expand Up @@ -112,33 +116,3 @@ data:
"type": "object"
}
}
---
apiVersion: v1
kind: ConfigMap
metadata:
name: openfga-init
data:
init.sh: |
#!/bin/sh
set -e
OPENFGA_URL='http://openfga.default.svc.cluster.local:8080'
until [ "$(curl -s -o /dev/null -w "%{http_code}" $OPENFGA_URL/stores)" -eq 200 ]; do
sleep 5
done
curl -sS -X POST ${OPENFGA_URL}/stores \
-H 'Content-Type: application/json' \
-d '{"name": "dev"}' | jq -r '.id' > /data/OPENFGA_STORE_ID
openfga_store_id=$(cat /data/OPENFGA_STORE_ID)
/usr/bin/identity-platform-admin-ui create-fga-model \
--fga-api-url "$OPENFGA_URL" \
--fga-api-token "42" \
--fga-store-id "$openfga_store_id"
curl -sS ${OPENFGA_URL}/stores/"$openfga_store_id"/authorization-models \
| jq -r '.authorization_models[0].id' > /data/OPENFGA_AUTHORIZATION_MODEL_ID
16 changes: 1 addition & 15 deletions deployments/kubectl/deployment.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -19,20 +19,10 @@ spec:
prometheus.io/scrape: "true"
prometheus.io/port: "8000"
spec:
initContainers:
- name: init
image: busybox
command: [ "/bin/sh", "-c", "while [ ! -s /data/openfga/OPENFGA_STORE_ID ] || [ ! -s /data/openfga/OPENFGA_AUTHORIZATION_MODEL_ID ]; do sleep 5; done" ]
volumeMounts:
- mountPath: /data/openfga
name: openfga-pv-data
containers:
- image: identity-platform-admin-ui
name: identity-platform-admin-ui
command: [ "/bin/sh", "-c", "export OPENFGA_STORE_ID=$(cat /data/OPENFGA_STORE_ID) OPENFGA_AUTHORIZATION_MODEL_ID=$(cat /data/OPENFGA_AUTHORIZATION_MODEL_ID); /usr/bin/identity-platform-admin-ui serve" ]
volumeMounts:
- mountPath: /data
name: openfga-pv-data
command: ["/usr/bin/identity-platform-admin-ui", "serve"]
envFrom:
- configMapRef:
name: identity-platform-admin-ui
Expand All @@ -57,7 +47,3 @@ spec:
periodSeconds: 30
imagePullSecrets:
- name: regcred-github
volumes:
- name: openfga-pv-data
persistentVolumeClaim:
claimName: openfga-pv-claim
19 changes: 3 additions & 16 deletions deployments/kubectl/job.yaml
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
---
apiVersion: batch/v1

kind: Job
metadata:
name: openfga-init
name: admin-ui-openfga-setup
spec:
template:
spec:
Expand All @@ -12,18 +11,6 @@ spec:
containers:
- name: job
image: identity-platform-admin-ui
command: [ "/bin/sh", "-c", "/scripts/init.sh" ]
volumeMounts:
- mountPath: /scripts
name: openfga-init
- mountPath: /data
name: openfga-pv
command: ["/usr/bin/identity-platform-admin-ui", "create-fga-model", "--fga-api-url", "http://openfga.default.svc.cluster.local:8080", "--fga-api-token", "42", "--store-k8s-configmap-resource", "default/identity-platform-admin-ui"]

restartPolicy: Never
volumes:
- name: openfga-init
configMap:
name: openfga-init
defaultMode: 0755
- name: openfga-pv
persistentVolumeClaim:
claimName: openfga-pv-claim
12 changes: 0 additions & 12 deletions deployments/kubectl/storage.yaml

This file was deleted.

1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ require (
github.com/grpc-ecosystem/grpc-gateway/v2 v2.18.1 // indirect
github.com/hashicorp/errwrap v1.1.0 // indirect
github.com/hashicorp/go-multierror v1.1.1 // indirect
github.com/imdario/mergo v0.3.6 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,8 @@ github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY
github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
github.com/imdario/mergo v0.3.6 h1:xTNEAn+kxVO7dTZGu0CegyqKZmoWFI0rF8UxjlB2d28=
github.com/imdario/mergo v0.3.6/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/jarcoal/httpmock v1.3.1 h1:iUx3whfZWVf3jT01hQTO/Eo5sAYtB2/rqaUuOtpInww=
Expand Down
22 changes: 16 additions & 6 deletions internal/k8s/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,18 +7,28 @@ import (
"k8s.io/client-go/kubernetes"
coreV1 "k8s.io/client-go/kubernetes/typed/core/v1"
"k8s.io/client-go/rest"
"k8s.io/client-go/tools/clientcmd"
)

func NewCoreV1Client() (coreV1.CoreV1Interface, error) {
func NewCoreV1Client(kubeconfig string) (coreV1.CoreV1Interface, error) {
// httpClient := new(http.Client)
// httpClient.Transport = otelhttp.NewTransport(http.DefaultTransport)

// creates the in-cluster config
config, err := rest.InClusterConfig()
if err != nil {
return nil, err
}
var config *rest.Config
var err error

if kubeconfig != "" {
// use the current context in kubeconfig
if config, err = clientcmd.BuildConfigFromFlags("", kubeconfig); err != nil {
return nil, err
}
} else {
// creates the in-cluster config
config, err = rest.InClusterConfig()
if err != nil {
return nil, err
}
}
// creates the clientset
// clientset, err := kubernetes.NewForConfigAndClient(config, httpClient)
clientset, err := kubernetes.NewForConfig(config)
Expand Down
Loading

0 comments on commit 588680c

Please sign in to comment.