p2-inc / keycloak-events

Useful Keycloak event listener implementations and utilities.
https://phasetwo.io
Other
179 stars 34 forks source link

Recommendation for dev workflow with JavaScript event handlers #8

Open pReya opened 1 year ago

pReya commented 1 year ago

I appreciate your work, and having the option to write Keycloak event handlers in JavaScript is a great addition for me. I was wondering if you have any recommendations regarding the development workflow when creating a new JS event handler. Since the code needs to be stored as a realm attribute, it can only be deployed via API call (is this a correct assumption? I did not find a way to edit realm attributes in the Admin console GUI, right?).

Deploying code via API call is a little clunky. How do you normally develop new event handlers? Just write them locally (in Node? Or run them in Nashorn?) and then deploy them through any API tool/CLI and hope that everything works in production?

Or is there any way to run event handlers from local javascript files? So I could mount the script file into the container, and iterate on it faster, without calling the API.

xgp commented 1 year ago

Hi @pReya. You're correct in that there is no admin UI way to edit realm attributes.

For customers that need realm import and incremental updates, we've used this as an init-container (if you're using k8s) or to run independently: https://github.com/adorsys/keycloak-config-cli It will diff the realm and only update the attributes that have change. However, beware your escaping, and make sure to test before you run in production.

xgp commented 1 year ago

Or is there any way to run event handlers from local javascript files? So I could mount the script file into the container, and iterate on it faster, without calling the API.

This is a good idea for development, but something beyond what I have time to do right now. I'll keep this open so that I can consider it in the future.

datenzar commented 1 year ago

Hi @pReya,

we're using an Infrastructure-as-Code approach with Terraform and have it integrated in our CI-pipelines. This allows us to process config adjustments in Keycloak via git and apply a 4-eyes principle via code review.

To give you quickstart with Terraform, have a look at the following snippet, which will create a new realm with a sample user and register the content of a scriptfile as event handler

terraform {
  backend "local" {
    path = ".terraform/.tfstate"
  }

  required_providers {
    keycloak = {
      source  = "mrparkers/keycloak"
      version = "4.2.0"
    }
  }
}

variable "server" {
  type        = string
  description = "The URL of the Keycloak server"
  default     = "http://localhost:8080"
}

provider "keycloak" {
  client_id = "admin-cli"
  username  = "admin"
  password  = "admin"
  url       = var.server
}

resource "keycloak_realm" "event_test" {
  realm                       = "keycloak-event-test-realm"
  enabled                     = true
  default_signature_algorithm = "RS256"

  attributes = {
    "_providerConfig.ext-event-script.0" = jsonencode({
      "scriptCode"        = file("${path.module}/log_event.js")
      "scriptName"        = "log-event"
      "scriptDescription" = "Logs events to the console"
    })
  }
}

resource "keycloak_realm_events" "realm_events" {
  realm_id = keycloak_realm.event_test.id

  events_enabled    = true
  events_expiration = 3600

  admin_events_enabled         = true
  admin_events_details_enabled = true

  # When omitted or left empty, keycloak will enable all event types
  enabled_event_types = [
    "LOGIN",
    "LOGOUT",
  ]

  events_listeners = [
    "jboss-logging", # keycloak enables the 'jboss-logging' event listener by default.
    "ext-event-webhook",
    "ext-event-script",
  ]
}

# existing user on test realm with key domain
# won't be forwarded
resource "keycloak_user" "test" {
  realm_id = keycloak_realm.event_test.id
  enabled  = true

  username   = "test"
  email      = "test@example.com"
  first_name = "Test"
  last_name  = "User"

  initial_password {
    value     = "test"
    temporary = false
  }
}