Skip to content

Zanzibar-like authorization with SpiceDB and Authorino (demo)

Notifications You must be signed in to change notification settings

guicassolato/authorino-spicedb

Repository files navigation

Demo: Zanzibar-like authorization with SpiceDB and Authorino

This is a demo of integrating Authorino with Authzed's SpiceDB.

SpiceDB is a Google Zanzibar-inspired authorization system that, like Google Zanzibar, allows for the modeling of fine-grained permissions based on relationships (Relationship-Based Access Control, or ReBAC).

One of the main challenges of implementing fine-grained permissions with an external authorization system is making that system aware of the existing relations. In this demo, we use Authorino callbacks to inform SpiceDB about the permissions implied by the operations requested by the users, such as creating or deleting an application resource, as well as granting and revoking access to resources for third-party users.

The full scope of the demo consists of protecting endpoints of a REST API that handles documents, the Docs API. Any authenticated user with a valid API key is allowed to create documents. Users can read and delete their own documents, as well as grant read access to their documents for other users. All fine-grained permissions involved are automatically stored in SpiceDB by Authorino, based on the operations requested by the users to the Docs API.

Requirements

The stack

  • Kubernetes cluster
    Started locally with Kind.
  • Docs API
    A REST API application that will be protected using SpiceDB and Authorino.
    The following HTTP endpoints are available:
    GET /docs          List all docs
    GET /docs/{id}     Read a doc
    POST /docs/{id}    Create a doc
    DELETE /docs/{id}  Delete a doc
    
  • Envoy proxy
    Deployed as sidecar of the Docs API, to serve the application with the External Authorization filter enabled and pointing to Authorino. After deploying the sidecar, the following additional endpoints are introduced:
    POST /docs/{id}/allow/{user}    Grant read access to the doc
    DELETE /docs/{id}/allow/{user}  Revoke read access to the doc
    
  • Authorino Operator
    Cluster-wide installation of the operator and CRDs to manage and use Authorino authorization services.
  • Authorino
    The external authorization service, deployed in namespaced reconciliation mode, in the same K8s namespace as the Docs API.
  • SpiceDB
    Open source Zanzibar-inspired database to store, compute, and validate fine grained permissions.
  • Contour
    Kubernetes Ingress Controller based on the Envoy proxy, to handle the ingress traffic to the Docs API and to Keycloak.

Architecture

Note: For simplicity, in the demo all components are deployed without TLS.

Setup

🅰 Create the cluster
kind create cluster --name authorino-demo --config -<<EOF
apiVersion: kind.x-k8s.io/v1alpha4
kind: Cluster
nodes:
- role: control-plane
  extraPortMappings:
  - containerPort: 80
    hostPort: 80
    listenAddress: "0.0.0.0"
  - containerPort: 443
    hostPort: 443
    listenAddress: "0.0.0.0"
EOF
🅱 Install Contour
kubectl apply -f https://raw.githubusercontent.com/guicassolato/authorino-spicedb/main/contour.yaml
🅲 Install the Authorino Operator
kubectl apply -f https://raw.githubusercontent.com/Kuadrant/authorino-operator/main/config/deploy/manifests.yaml

Note: In OpenShift, the Authorino Operator can alternatively be installed directly from the Red Hat OperatorHub, using Operator Lifecycle Manager.

Run the demo ① → ④

① Deploy the Docs API

🅰 Create the namespace
kubectl create namespace docs-api
🅱 Deploy the Docs API in the namespace
kubectl -n docs-api apply -f -<<EOF
apiVersion: apps/v1
kind: Deployment
metadata:
  name: docs-api
  labels:
    app: docs-api
spec:
  selector:
    matchLabels:
      app: docs-api
  template:
    metadata:
      labels:
        app: docs-api
    spec:
      containers:
        - name: docs-api
          image: quay.io/kuadrant/authorino-examples:docs-api
          imagePullPolicy: IfNotPresent
          env:
            - name: PORT
              value: "3000"
          tty: true
          ports:
            - containerPort: 3000
  replicas: 1
---
apiVersion: v1
kind: Service
metadata:
  name: docs-api
  labels:
    app: docs-api
spec:
  selector:
    app: docs-api
  ports:
    - name: http
      port: 3000
      protocol: TCP
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: docs-api
  labels:
    app: docs-api
spec:
  rules:
    - host: docs-api.127.0.0.1.nip.io
      http:
        paths:
          - backend:
              service:
                name: docs-api
                port:
                  number: 3000
            path: /docs
            pathType: Prefix
EOF
🅲 Try the Docs API unprotected
curl http://docs-api.127.0.0.1.nip.io/docs -i
# HTTP/1.1 200 OK

② Create the permissions database

🅰 Create the namespace
kubectl create namespace spicedb
🅱 Deploy the SpiceDB instance
kubectl -n spicedb apply -f -<<EOF
apiVersion: apps/v1
kind: Deployment
metadata:
  name: spicedb
  labels:
    app: spicedb
spec:
  selector:
    matchLabels:
      app: spicedb
  template:
    metadata:
      labels:
        app: spicedb
    spec:
      containers:
        - name: spicedb
          image: authzed/spicedb
          args:
            - serve
            - "--grpc-preshared-key"
            - secret
            - "--http-enabled"
          ports:
            - containerPort: 50051
            - containerPort: 8443
  replicas: 1
---
apiVersion: v1
kind: Service
metadata:
  name: spicedb
spec:
  selector:
    app: spicedb
  ports:
    - name: grpc
      port: 50051
      protocol: TCP
    - name: http
      port: 8443
      protocol: TCP
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: spicedb
  labels:
    app: spicedb
spec:
  rules:
    - host: spicedb.127.0.0.1.nip.io
      http:
        paths:
          - backend:
              service:
                name: spicedb
                port:
                  number: 8443
            path: /
            pathType: Prefix
EOF
🅲 Create the permission schema
curl -X POST http://spicedb.127.0.0.1.nip.io/v1/schema/write \
     -H 'Authorization: Bearer secret' \
     -H 'Content-Type: application/json' \
     -d @- <<EOF
{
  "schema": "definition user {}\ndefinition doc {\n\trelation reader: user\n\trelation writer: user\n\n\tpermission read = reader + writer\n\tpermission write = writer\n}"
}
EOF

③ Lock down the Docs API

🅰 Request an instance of Authorino
kubectl -n docs-api apply -f -<<EOF
apiVersion: operator.authorino.kuadrant.io/v1beta1
kind: Authorino
metadata:
  name: authorino
spec:
  listener:
    tls:
      enabled: false
  oidcServer:
    tls:
      enabled: false
EOF
🅱 Redeploy the Docs API with the sidecar proxy
kubectl -n docs-api apply -f -<<EOF
apiVersion: apps/v1
kind: Deployment
metadata:
  name: docs-api
  labels:
    app: docs-api
spec:
  selector:
    matchLabels:
      app: docs-api
  template:
    metadata:
      labels:
        app: docs-api
    spec:
      containers:
        - name: docs-api
          image: quay.io/kuadrant/authorino-examples:docs-api
          imagePullPolicy: IfNotPresent
          env:
            - name: PORT
              value: "3000"
          tty: true
          ports:
            - containerPort: 3000
        - name: envoy
          image: envoyproxy/envoy:v1.19-latest
          imagePullPolicy: IfNotPresent
          command:
            - /usr/local/bin/envoy
          args:
            - --config-path /usr/local/etc/envoy/envoy.yaml
            - --service-cluster front-proxy
            - --log-level info
            - --component-log-level filter:trace,http:debug,router:debug
          ports:
            - containerPort: 8000
          volumeMounts:
            - mountPath: /usr/local/etc/envoy
              name: config
              readOnly: true
      volumes:
        - name: config
          configMap:
            items:
              - key: envoy.yaml
                path: envoy.yaml
            name: envoy
  replicas: 1
---
apiVersion: v1
kind: Service
metadata:
  name: docs-api
  labels:
    app: docs-api
spec:
  selector:
    app: docs-api
  ports:
    - name: envoy
      port: 8000
      protocol: TCP
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: docs-api
  labels:
    app: docs-api
spec:
  rules:
    - host: docs-api.127.0.0.1.nip.io
      http:
        paths:
          - backend:
              service:
                name: docs-api
                port:
                  number: 8000
            path: /docs
            pathType: Prefix
---
apiVersion: v1
kind: ConfigMap
metadata:
  name: envoy
  labels:
    app: envoy
data:
  envoy.yaml: |
    static_resources:
      clusters:
        - name: docs-api
          connect_timeout: 0.25s
          type: strict_dns
          lb_policy: round_robin
          load_assignment:
            cluster_name: docs-api
            endpoints:
              - lb_endpoints:
                  - endpoint:
                      address:
                        socket_address:
                          address: 127.0.0.1
                          port_value: 3000
        - name: authorino
          connect_timeout: 0.25s
          type: strict_dns
          lb_policy: round_robin
          http2_protocol_options: {}
          load_assignment:
            cluster_name: authorino
            endpoints:
              - lb_endpoints:
                  - endpoint:
                      address:
                        socket_address:
                          address: authorino-authorino-authorization
                          port_value: 50051
      listeners:
        - address:
            socket_address:
              address: 0.0.0.0
              port_value: 8000
          filter_chains:
            - filters:
                - name: envoy.http_connection_manager
                  typed_config:
                    "@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
                    stat_prefix: local
                    route_config:
                      name: docs-api
                      virtual_hosts:
                        - name: docs-api
                          domains: ['*']
                          routes:
                            - match:
                                prefix: /
                              route:
                                cluster: docs-api
                    http_filters:
                      - name: envoy.filters.http.ext_authz
                        typed_config:
                          "@type": type.googleapis.com/envoy.extensions.filters.http.ext_authz.v3.ExtAuthz
                          transport_api_version: V3
                          failure_mode_allow: false
                          include_peer_certificate: true
                          grpc_service:
                            envoy_grpc:
                              cluster_name: authorino
                            timeout: 1s
                      - name: envoy.filters.http.lua
                        typed_config:
                          "@type": type.googleapis.com/envoy.extensions.filters.http.lua.v3.Lua
                          inline_code: |
                            function envoy_on_request(request_handle)
                              if string.match(request_handle:headers():get(":path"), '^/docs/[^/]+/allow/.+') then
                                request_handle:respond({[":status"] = "200"}, "")
                              end
                            end
                      - name: envoy.filters.http.router
                        typed_config: {}
                    use_remote_address: true
    admin:
      access_log_path: "/tmp/admin_access.log"
      address:
        socket_address:
          address: 0.0.0.0
          port_value: 8001
EOF
🅲 Try the Docs API without authentication
curl http://docs-api.127.0.0.1.nip.io/docs -i
# HTTP/1.1 404 Not Found
# x-ext-auth-reason: Service not found
# server: envoy
# ...

④ Open up the Docs API for authenticated and authorized users

🅰 Create the AuthConfig
kubectl -n docs-api apply -f -<<EOF
apiVersion: authorino.kuadrant.io/v1beta1
kind: AuthConfig
metadata:
  name: docs-api-protection
spec:
  hosts:
    - docs-api.127.0.0.1.nip.io

  patterns:
    create:
      - selector: context.request.http.method
        operator: eq
        value: POST
      - selector: context.request.http.path.@extract:{"sep":"/","pos":3}
        operator: neq
        value: allow
    list:
      - selector: context.request.http.method
        operator: eq
        value: GET
      - selector: context.request.http.path.@extract:{"sep":"/","pos":2}
        operator: eq
        value: ""

  # Users authenticated with API keys
  identity:
    - name: api-key-users
      apiKey:
        selector:
          matchLabels:
            app: docs-api
      credentials:
        in: authorization_header
        keySelector: APIKEY

  metadata:
    # List resources → lookup resources the user has read access to
    - name: permission-lookup-read
      when:
        - patternRef: list
      http:
        endpoint: http://spicedb.spicedb.svc.cluster.local:8443/v1/permissions/resources
        method: POST
        contentType: application/json
        body:
          valueFrom:
            authJSON: |
              \{
                "resourceObjectType":"doc",
                "permission":"read",
                "subject":\{
                  "object":\{
                    "objectType":"user",
                    "objectId":"{auth.identity.metadata.annotations.username}"
                  \}
                \}
              \}
        sharedSecretRef:
          name: spicedb
          key: token

    # Create resource → lookup resources the user has write access to
    - name: permission-lookup-write
      when:
        - patternRef: create
      http:
        endpoint: http://spicedb.spicedb.svc.cluster.local:8443/v1/permissions/subjects
        method: POST
        contentType: application/json
        body:
          valueFrom:
            authJSON: |
              \{
                "resource": \{
                  "objectType": "doc",
                  "objectId": "{context.request.http.path.@extract:{"sep":"/","pos":2}}"
                \},
                "permission": "write",
                "subjectObjectType": "user"
              \}
        sharedSecretRef:
          name: spicedb
          key: token

  authorization:
    # Read or delete a resource → check in SpiceDB if the user has read or write permission respectively
    - name: read-or-delete-resource
      when:
        - selector: context.request.http.method
          operator: neq
          value: POST
        - selector: context.request.http.path.@extract:{"sep":"/","pos":2}
          operator: neq
          value: ""
        - selector: context.request.http.path.@extract:{"sep":"/","pos":3}
          operator: neq
          value: allow
      authzed:
        endpoint: spicedb.spicedb.svc.cluster.local:50051
        insecure: true
        sharedSecretRef:
          name: spicedb
          key: token
        subject:
          kind:
            value: user
          name:
            valueFrom:
              authJSON: auth.identity.metadata.annotations.username
        resource:
          kind:
            value: doc
          name:
            valueFrom:
              authJSON: context.request.http.path.@extract:{"sep":"/","pos":2}
        permission:
          valueFrom:
            authJSON: context.request.http.method.@replace:{"old":"GET","new":"read"}.@replace:{"old":"DELETE","new":"write"}

    # Create a resource → ensure the writer relationship does not exist in SpiceDB
    - name: create-resource
      when:
        - patternRef: create
      json:
        rules:
          - selector: auth.metadata.permission-lookup-write.result
            operator: eq
            value: ""

    # Grant or revoke access to resource → check in SpiceDB if the user has write permission
    - name: grant-or-revoke-access-to-resource
      when:
        - selector: context.request.http.path.@extract:{"sep":"/","pos":3}
          operator: eq
          value: allow
      authzed:
        endpoint: spicedb.spicedb.svc.cluster.local:50051
        insecure: true
        sharedSecretRef:
          name: spicedb
          key: token
        subject:
          kind:
            value: user
          name:
            valueFrom:
              authJSON: auth.identity.metadata.annotations.username
        resource:
          kind:
            value: doc
          name:
            valueFrom:
              authJSON: context.request.http.path.@extract:{"sep":"/","pos":2}
        permission:
          value: write

  response:
    # Create new resource → inject user info in the request
    - name: x-ext-auth-data
      when:
        - patternRef: create
      json:
        properties:
          - name: author
            valueFrom: { authJSON: auth.identity.metadata.annotations.fullname }
          - name: user_id
            valueFrom: { authJSON: auth.identity.metadata.annotations.username }

    # List resources → filter resource ids the user has access to
    - name: x-filter
      when:
        - patternRef: list
      json:
        properties:
          - name: id
            valueFrom:
              authJSON: auth.metadata.permission-lookup-read.result.resourceObjectId
          - name: ids
            valueFrom:
              authJSON: auth.metadata.permission-lookup-read.#.result.resourceObjectId

  callbacks:
    # Create new resource → create 'writer' relationship in SpiceDB
    - name: create-resource
      when:
        - selector: auth.authorization.create-resource
          operator: neq
          value: ""
      http:
        endpoint: http://spicedb.spicedb.svc.cluster.local:8443/v1/relationships/write
        method: POST
        contentType: application/json
        body:
          valueFrom:
            authJSON: |
              \{
                "updates":[
                  \{
                    "operation":"OPERATION_CREATE",
                    "relationship":\{
                      "resource":\{
                        "objectType":"doc",
                        "objectId":"{context.request.http.path.@extract:{"sep":"/","pos":2}}"
                      \},
                      "relation":"writer",
                      "subject":\{
                        "object":\{
                          "objectType":"user",
                          "objectId":"{auth.identity.metadata.annotations.username}"
                        \}
                      \}
                    \}
                  \}
                ]
              \}
        sharedSecretRef:
          name: spicedb
          key: token

    # Delete resource → delete all corresponding relationships in SpiceDB
    - name: delete-resource
      when:
        - selector: auth.authorization.read-or-delete-resource
          operator: neq
          value: ""
        - selector: context.request.http.method
          operator: eq
          value: DELETE
      http:
        endpoint: http://spicedb.spicedb.svc.cluster.local:8443/v1/relationships/delete
        method: POST
        contentType: application/json
        body:
          valueFrom:
            authJSON: |
              \{
                "relationshipFilter": \{
                  "resourceType": "doc",
                  "optionalResourceId": "{context.request.http.path.@extract:{"sep":"/","pos":2}}"
                \}
              \}
        sharedSecretRef:
          name: spicedb
          key: token

    # Grant access to resource → create 'reader' relationship in SpiceDB
    - name: grant-access
      when:
        - selector: auth.authorization.grant-or-revoke-access-to-resource
          operator: neq
          value: ""
        - selector: context.request.http.method
          operator: eq
          value: POST
      http:
        endpoint: http://spicedb.spicedb.svc.cluster.local:8443/v1/relationships/write
        method: POST
        contentType: application/json
        body:
          valueFrom:
            authJSON: |
              \{
                "updates":[
                  \{
                    "operation":"OPERATION_CREATE",
                    "relationship":\{
                      "resource":\{
                        "objectType":"doc",
                        "objectId":"{context.request.http.path.@extract:{"sep":"/","pos":2}}"
                      \},
                      "relation":"reader",
                      "subject":\{
                        "object":\{
                          "objectType":"user",
                          "objectId":"{context.request.http.path.@extract:{"sep":"/","pos":4}}"
                        \}
                      \}
                    \}
                  \}
                ]
              \}
        sharedSecretRef:
          name: spicedb
          key: token

    # Revoke access to resource → delete 'reader' relationships in SpiceDB
    - name: revoke-access
      when:
        - selector: auth.authorization.grant-or-revoke-access-to-resource
          operator: neq
          value: ""
        - selector: context.request.http.method
          operator: eq
          value: DELETE
      http:
        endpoint: http://spicedb.spicedb.svc.cluster.local:8443/v1/relationships/delete
        method: POST
        contentType: application/json
        body:
          valueFrom:
            authJSON: |
              \{
                "relationshipFilter": \{
                  "resourceType": "doc",
                  "optionalResourceId": "{context.request.http.path.@extract:{"sep":"/","pos":2}}",
                  "optionalRelation": "reader",
                  "optionalSubjectFilter": \{
                    "subjectType": "user",
                    "optionalSubjectId": "{context.request.http.path.@extract:{"sep":"/","pos":4}}"
                  \}
                \}
              \}
        sharedSecretRef:
          name: spicedb
          key: token
---
apiVersion: v1
kind: Secret
metadata:
  name: spicedb
  labels:
    app: spicedb
stringData:
  token: secret
EOF
🅱 Create the API keys for users to consume the Docs API
kubectl -n docs-api apply -f -<<EOF
apiVersion: v1
kind: Secret
metadata:
  name: api-key-writer
  labels:
    authorino.kuadrant.io/managed-by: authorino
    app: docs-api
  annotations:
    username: emilia
    fullname: 👩🏾 Emilia Jones
stringData:
  api_key: IAMEMILIA
---
apiVersion: v1
kind: Secret
metadata:
  name: api-key-reader
  labels:
    authorino.kuadrant.io/managed-by: authorino
    app: docs-api
  annotations:
    username: beatrice
    fullname: 🧑🏻‍🦰 Beatrice Smith
stringData:
  api_key: IAMBEATRICE
EOF
🅲 Consume the Docs API fully protected

As 👩🏾 Emilia, create a doc:

What should happen?

Create a doc

curl -H 'Authorization: APIKEY IAMEMILIA' \
   -X POST \
   -H 'Content-Type: application/json' \
   -d '{"title":"Emilia´s doc","body":"This is Emilia´s doc."}' \
   http://docs-api.127.0.0.1.nip.io/docs/123 -i
# HTTP/1.1 200 OK
# ...
# {"id":"123","title":"Emilia´s doc","body":"This is Emilia´s doc.","date":"2023-02-07 18:17:30 +0000","author":"👩🏾 Emilia Jones","user_id":"emilia"}

As 👩🏾 Emilia, read the doc just created:

What should happen?

Read the doc OK

curl -H 'Authorization: APIKEY IAMEMILIA' \
   -X GET \
   http://docs-api.127.0.0.1.nip.io/docs/123 -i
# HTTP/1.1 200 OK

As 🧑🏻‍🦰 Beatrice, try to read the doc created by Emilia:

What should happen?

Read the doc NOK

curl -H 'Authorization: APIKEY IAMBEATRICE' \
   -X GET \
   http://docs-api.127.0.0.1.nip.io/docs/123 -i
# HTTP/1.1 403 Forbidden
# x-ext-auth-reason: PERMISSIONSHIP_NO_PERMISSION;token=...

As 👩🏾 Emilia, grant access to the doc for 🧑🏻‍🦰 Beatrice:

What should happen?

Grant access

curl -H 'Authorization: APIKEY IAMEMILIA' \
   -X POST \
   http://docs-api.127.0.0.1.nip.io/docs/123/allow/beatrice -i
# HTTP/1.1 200 OK

As 🧑🏻‍🦰 Beatrice, try again to read the doc owned by Emilia:

curl -H 'Authorization: APIKEY IAMBEATRICE' \
   -X GET \
   http://docs-api.127.0.0.1.nip.io/docs/123 -i
# HTTP/1.1 200 OK

As 🧑🏻‍🦰 Beatrice, create a doc of her own:

curl -H 'Authorization: APIKEY IAMBEATRICE' \
   -X POST \
   -H 'Content-Type: application/json' \
   -d '{"title":"Beatrice´s doc","body":"This is Beatrice´s doc."}' \
   http://docs-api.127.0.0.1.nip.io/docs/456 -i
# HTTP/1.1 200 OK
# ...
# {"id":"456","title":"Beatrice´s doc","body":"This is Beatrice´s doc.","date":"2023-02-07 18:25:10 +0000","author":"🧑🏻‍🦰 Beatrice Smith","user_id":"beatrice"}

As 🧑🏻‍🦰 Beatrice, list all the docs Beatrice has access to:

What should happen?

List docs

curl -H 'Authorization: APIKEY IAMBEATRICE' \
   http://docs-api.127.0.0.1.nip.io/docs -i
# HTTP/1.1 200 OK
# ...
# [
#   {"id":"123","title":"Emilia´s doc","body":"This is Emilia´s doc.","date":"2023-02-07 18:17:30 +0000","author":"👩🏾 Emilia Jones","user_id":"emilia"},
#   {"id":"456","title":"Beatrice´s doc","body":"This is Beatrice´s doc.","date":"2023-02-07 18:25:10 +0000","author":"🧑🏻‍🦰 Beatrice Smith","user_id":"beatrice"}
# ]

As 👩🏾 Emilia, list all the docs Emilia has access to:

curl -H 'Authorization: APIKEY IAMEMILIA' \
   http://docs-api.127.0.0.1.nip.io/docs -i
# HTTP/1.1 200 OK
# ...
# [{"id":"123","title":"Emilia´s doc","body":"This is Emilia´s doc.","date":"2023-02-07 18:17:30 +0000","author":"👩🏾 Emilia Jones","user_id":"emilia"}]

As 👩🏾 Emilia, revoke 🧑🏻‍🦰 Beatrice's access to the doc:

What should happen?

Revoke access

curl -H 'Authorization: APIKEY IAMEMILIA' \
   -X DELETE \
   http://docs-api.127.0.0.1.nip.io/docs/123/allow/beatrice -i
# HTTP/1.1 200 OK

As 🧑🏻‍🦰 Beatrice, list again the docs Beatrice has access to:

curl -H 'Authorization: APIKEY IAMBEATRICE' \
   http://docs-api.127.0.0.1.nip.io/docs -i
# HTTP/1.1 200 OK
# ...
# [{"id":"456","title":"Beatrice´s doc","body":"This is Beatrice´s doc.","date":"2023-02-07 18:25:10 +0000","author":"🧑🏻‍🦰 Beatrice Smith","user_id":"beatrice"}]

As 🧑🏻‍🦰 Beatrice, try one last time to read the doc owned by Emilia:

curl -H 'Authorization: APIKEY IAMBEATRICE' \
   -X GET \
   http://docs-api.127.0.0.1.nip.io/docs/123 -i
# HTTP/1.1 403 Forbidden
# x-ext-auth-reason: PERMISSIONSHIP_NO_PERMISSION;token=...

As 👩🏾 Emilia, delete the doc:

What should happen?

Delete the doc

curl -H 'Authorization: APIKEY IAMEMILIA' \
   -X DELETE \
   http://docs-api.127.0.0.1.nip.io/docs/123 -i
# HTTP/1.1 200 OK

As 👩🏾 Emilia, retry to read the doc just deleted:

curl -H 'Authorization: APIKEY IAMEMILIA' \
   -X GET \
   http://docs-api.127.0.0.1.nip.io/docs/123 -i
# HTTP/1.1 403 Forbidden
# x-ext-auth-reason: PERMISSIONSHIP_NO_PERMISSION;token=...

Cleanup

kind delete cluster --name authorino-demo

Caveats

Consistency

Because Authorino builds in SpiceDB the permission relationships implied by the requests sent to the Docs API before these requests effectively hit the application, and at the same time the application itself has no knowledge of the authorization system in place at all, the system may run into a situation where the resources and relations stored in the application mismatch the state of the relationships in SpiceDB. This can happen, for example, if an authorized request (i.e. after passing Authorino) fails to be processed by the application due to a server error.

To mitigate the risk of consistency issues, the HTTP requests sent to the Docs API must be treated as an atomic transaction from the moment Envoy hands it over to Authorino, until the upstream application response is processed by Envoy.

To be able to recover from possible consistency issues in case the mitigation fails, logs of the requests handled by Authorino can be stored including timestamp, username, as well as method and path of the HTTP request. Such logs can be implemented by adding another Authorino callback in the AuthConfig. The system should occasionally check for consistency issues and use the logs to rebuild the desired state incrementally.

Latency

Compared to monolithic approach of embedded authorization rules and without proxy in the middle, another caveat of this implementation comes from the extra hops involved in the communication between sidecar proxy (Envoy) and authorization service (Authorino), authorization service and permission database/policy engine (SpiceDB), and sidecar proxy and upstream application (Docs API), and its significance in terms of added latency to the overall request.

To mitigate the impact on latency especially due to the HTTP and GRPC communication between Authorino and SpiceDB, caching can be enabled in Authorino for the metadata and authorization rules.

Performance can also be improved once callbacks in the AuthConfig can be processed asynchronously (see Kuadrant/authorino#369).

About

Zanzibar-like authorization with SpiceDB and Authorino (demo)

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published