By Kevin Schneider
Originally posted on https://medium.com/@kevsuisse
Configuration management for ForgeRock Identity Cloud. Part 1
This series of posts explores how to best handle configuration for ForgeRock Identity Cloud. It aims to provide a solution that both enhances the developer experience and enables organisations to know exactly what they configure in ForgeRock Identity Cloud. It argues for a common approach to handle configuration objects to enable an ecosystem of tools.
Configuration Management in ForgeRock Identity CloudWhy is configuration management necessary?
ForgeRock Identity Cloud already provides a range of features that help customers handle the configuration of their tenants. Tenants can be configured via the admin interface and environments are linked via the promotion process.
Configuration is split into two types. Dynamic configuration is everything that normally is dependent on the environment, such as application clients, user data etc. Static configuration on the other hand is the basic configuration of the tenant, such as journeys, services definitions, managed object definitions etc. Static configuration is the same across development, staging and production environments. Anything that is environment specific can be set up to use ESV placeholders, for example, to set a domain name per environment.
ForgeRock Identity Cloudβs promotion process promotes static configuration from dev to staging to production. The promotion can be kicked off via the admin UI or APIs which makes it easy to integrate into a configuration pipeline.
One major question this leaves open however is how the development environment gets configured in the first place. Developers could simply be given tenant admin rights and make their changes via the admin UI. The promotion API then gives some insights into what changes had been made. In simple setups with a small team, this approach might be all that is needed. However, it quickly starts to get more complicated:
- Itβs very hard to impossible to see the exact changes that have been made. For example, it is not possible to see that a scripted decision node has been changed to now use a different default outcome.
- An accidental change can be made unknowingly, which is then promoted to higher environments.
- The development experience is lacking. Scripts have to be developed in the in-browser IDE or in a local IDE and then manually copied over to the dev environment.
- Setting up a new environment based on the dev environment involves a lot of copying and manual adaptions. This is particularly an issue when using sandboxes, as they are not part of the promotion process
Sandboxes
Sandboxes provide a way for developers to test changes without the risk of pushing them to higher environments as they are detached from the promotion process. They are therefore a great development tool and are an essential part of the pipeline. However, tooling needs to be in place to easily set the starting point to the dev environment and be able to move changes to the dev environment and back.
A GIT-based flow
The solution to these issues is to adopt the principles of handling configuration as code. This means that all static configuration objects are stored in a customer-owned GIT repository. Instead of just changing the development environment directly, changes are first pushed to the repository and are then (automatically) reviewed and automatically pushed to the development environment. That repository acts as the source of truth and feeds the static configuration of the DEV environment. ForgeRock Identity Cloudβs REST interface allows importing and exporting of all configurations via the API and open source tools have been developed to do this with a simple command line interface. Later parts of this blog series will discuss some of these tools in detail.
The benefits of this solution are numerous
- Changes become immediately visible, and it is clear what is configured and what is not
- A change review process can be implemented based on the commits to the repository
- Multiple people can contribute to the configuration. The risk of overwriting each otherβs changes is minimized
- Multiple environments can be set up from the same repository. For example, multiple sandboxes and the dev environment, potentially using different release versions of the configuration
- Development can happen in fully fledged IDEs and normal development best practices can be applied to the scripts
The main question now is how this configuration should be structured to reap all these benefits.
Directory structure for config repo
The following directory structure enables an easy and self-explanatory location for each file. When working on the files, any changes should immediately make it clear what configuration objects have changed. This is important as it allows for efficient reviews of changes and makes it easier for the reviewer to understand the change and spot any issues. It also enables different tools to work from the same structure.
The goals of the repository directory structure are therefore
- Maintain order and content of configuration to enable the use of version control tools and workflows such as git tooling
- Break monolithic configuration into its constituent parts for more granular updates and management
- Represent configuration as readable and editable content (e.g. decode and break out inline scripts, HTML and CSS)
Separating native files from metadata wrappers
A guiding principle is that native files should be separate from the configuration wrapper. What does that mean?
Letβs take a look at email templates as an example.
Any individual template can consist of HTML,CSS style sheets and text templates. all of these elements are wrapped in a JSON structure which also provides metadata like the ID, name and description.
If you just store the JSON as a whole, it becomes very hard for changes to be made in the first place. But it is also very hard to see what has changed if for example the default link colour is changed in the CSS. Breaking the elements out of the wrapper into individual files allows one to effectively use an IDE with all its goodness such as autocompletion, format checking, linting etc. And when the change is committed, a reviewer can easily see that just the CSS has been affected and nothing else.
Where inline configuration is broken out into separate files, the top level configuration JSON is altered to use file instead of source for the content β e.g.
"onCreate": {
"globals": {},
"type": "text/javascript",
"source": "require('onCreateUser').setDefaultFields(object);"
}
is converted to
"onCreate": {
"globals": {},
"type": "text/javascript",
"file": "alpha_user.onCreate.js"
}
and the javascript source is moved to the corresponding file. A similar approach is taken for breaking out HTML, CSS etc.
What not to track
Certain configuration is best left untouched. While it is technically possible to customize these, there is a high chance of breaking the tenant or causing incompatibilities with future ForgeRock Identity Cloud updates. Examples of these are
- IDM Authentication config
- IDM repo.ds configuration
- Out of the box scripts like the default OAuth 2.0 Token modification scripts
File naming convention
For most file names it makes sense to use the object name of the REST interface. For example, openidm/config/managed becomes managed.json
Where the file name is created as part of the object creation, for example for scripts, templates etc, the following conventions have proven useful.
File naming should first and foremost align with any company guidelines. If there isnβt a good guideline, file names in kebab-case are frequently used.
Use a prefix for all your files. This makes it much easier to separate out of the box files from custom, customer owned ones.
An example script file name following these suggestions is
acme-collect-otp.js
Realms
Within the static configuration, a further distinction can be made between realm dependant configuration and global configuration. Typically, IDM configuration is global and AM configuration realm dependent. Password policies and themes are the exceptions.
Tree view of an example repo
βββ access-config
β βββ access.json
βββ cors
β βββ cors-config.json
βββ endpoints
β βββ acme-endpoint-test
β βββ acme-endpoint-test.js
β βββ acme-endpoint-test.json
βββ email-provider
β βββ external.email.json
βββ email-templates
β βββ forgottenUsername
β β βββ forgottenUsername.css
β β βββ forgottenUsername.en.html
β β βββ forgottenUsername.en.md
β β βββ forgottenUsername.fr.html
β β βββ forgottenUsername.fr.md
β β βββ forgottenUsername.json
βββ esvs
β βββ secrets
β βββ variables
β βββ esv-rcs-healthcheck-enabled.json
β βββ esv-rcs-healthcheck-schedule.json
βββ internal-roles
βββ kba
β βββ selfservice.kba.json
βββ managed-objects
β βββ alpha_application
β β βββ alpha_application.json
β βββ alpha_assignment
β β βββ alpha_assignment.json
β βββ alpha_group
β β βββ alpha_group.json
β βββ alpha_organization
β β βββ alpha_organization.json
β βββ alpha_role
β β βββ alpha_role.json
β βββ alpha_user
β β βββ alpha_user.json
β βββ bravo_application
β β βββ bravo_application.json
β βββ [β¦]
βββ realms
β βββ alpha
β β βββ journeys
β β β βββ Login
β β β β βββ Login.json
β β β β βββ nodes
β β β β βββ Data Store Decision - 2998c1c9-f4c8β4a00-b2c6β3426783ee49d.json
β β β β βββ Increment Login Count - bba3e0d8β8525β4e82-bf48-ac17f7988917.json
β β β β βββ Inner Tree Evaluator - 33b24514β3e50β4180β8f08-ab6f4e51b07e.json
β β β β βββ Page Node - a12bc72f-ad97β4f1e-a789-a1fa3dd566c8
β β β β β βββ Platform Password - 0c80c39b-4813β4e67-b4fb-5a0bba85f994.json
β β β β β βββ Platform Username - 7354982f-57b6β4b04β9ddc-f1dd1e1e07d0.json
β β β β βββ Page Node - a12bc72f-ad97β4f1e-a789-a1fa3dd566c8.json
β β βββ password-policy
β β β βββ alpha_user-password-policy.json
β β βββ realm-config
β β β βββ authentication.json
β β βββ scripts
β β β βββ acme-test-script.json
β β β βββ scripts-content
β β β βββ AUTHENTICATION_TREE_DECISION_NODE
β β β βββ acme-test-script.js
β β βββ services
β β β βββ SocialIdentityProviders.json
β β β βββ baseurl.json
β β β βββ oauth-oidc.json
β β β βββ policyconfiguration.json
β β β βββ selfServiceTrees.json
β β β βββ validation.json
β β βββ themes
β β βββ Contrast
β β β βββ Contrast.json
β β β βββ accountFooter.html
β β β βββ journeyFooter.html
β β β βββ journeyHeader.html
β βββ bravo
β βββ journeys
β βββ password-policy
β βββ realm-config
β βββ scripts
β βββ services
β βββ themes
βββ schedules
β βββ rcsHealthCheck
β β βββ rcsHealthCheck.js
β β βββ rcsHealthCheck.json
βββ sync
β βββ connectors
βββ terms-conditions
β βββ 1.0
β β βββ en.html
β βββ terms-conditions.json
βββ ui
βββ ui-configuration.json
Structure in detail
Root level structure
sync
Contains the connector and mapping definitions
Subdirectories as follows
- connectors contains the connector configuration, broken down into one JSON file per connector with the filename {connector-name}.json
- mappings contains the sync mappings broken down into one JSON file per mapping with the filename {mapping-name}.json
- rcs contains the remote connector server configuration provisioner.openicf.connectorinfoprovider.json
cors
Contains the combined CORS configuration for the tenant cors-config.json, with settings for both /am and /openidm endpoints
email-provider
The external email service configuration external.email.json
email-templates
One subdirectory {template-name} per email template. Each subdirectory contains the top level template configuration {template-name}.json, alongside separate files for
- CSS {template-name}.css
- HTML {template-name}.{lang}.html
- Markdown {template-name}.{lang}.md.
access-config
The authorisation configuration access.json for /openidm endpoints
endpoints
A subdirectory per endpoint, each subdirectory containing a JSON file with the endpoint configuration {endpoint-name}.json alongside the file {endpoint-name}.js containing the endpoint JavaScript.
schedules
A subdirectory per schedule, each subdirectory containing a JSON file with the schedule configuration {schedule-name}.json, alongside the script file {schedule-name}.js where applicable.
kba
The Knowledge Based Authentication configuration for the tenant as a single JSON file selfservice.kba.json
managed-objects
One subdirectory {object-name} for each managed object configured in the tenant. Each subdirectory contains a JSON file {object-name}.json with the top level managed object configuration, alongside a separate javascript file {object-name}.{event-trigger}.json for each managed object trigger script
terms-conditions
A JSON file with the top level terms and conditions configuration terms-conditions.json, alongside a subdirectory {terms-version} for each version configured. Each subdirectory has an HTML file {language}.html for each language configured
ui
A JSON file with the hosted UI configuration
esvs
Subdirectories as follows
- secrets- one JSON file{secret-name}.json per environment secret
- variables- one JSON file{variable-name}.jsonper environment variable
Each JSON file has a placeholder for the valueBase64 value.
internal-roles
One JSON file {role-name}.json per authorisation role configured in the tenant. This does not include the system roles such as openidm-admin etc (system roles are detected and excluded based on their having no configured privileges).
Realm level structure
The configuration repo contains the top level directory realms with a subdirectory for both the alpha and bravo realm. Each realm directory has the following subdirectories:
scripts
This directory contains one JSON file per script defined in this realm. The scripts themselves are created under script-content/{script-type}.
journeys
This contains a subdirectory {journey-name} for each journey configured in the realm. Each of these subdirectories contains the file {journey-name}.json with the overall tree configuration, together with a nodes subdirectory containing one file per node in the tree. Node config filenames are formatted as {node-display-name} β {tree-node-id}.json. In the case of Page nodes, all child nodes are contained within a subdirectory named: {page-node-display-name} β {page-tree-node-id}.json.
password-policy
{realm}_user-password-policy.json contains the realm password policy
services
This directory contains one file {service-name}.json per access management service configured in the realm.
themes
There is one subdirectory {theme-name} per theme defined in the realm. Each theme directory has the file {theme-name}.json containing the top level theme configuration, alongside separate HTML files containing the header and footer contents.
Conclusion
Driving ForgeRock Identity Cloud configuration from a GIT repository enables customers to make use of the benefits of configuration as code. The proposed directory structure and handling of configuration enable tools to be developed that can further improve the developer experience, such as CI/CD pipelines, IDE integrations and more.
In the next article, I will detail how to use make use of the repository by describing a CI/CD pipeline approach.