Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Realm export normalization #818

Draft
wants to merge 49 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
49 commits
Select commit Hold shift + click to select a range
5dbdcbf
Realm export: introduce command line options
sonOfRa Nov 3, 2022
c7e759d
Compare and import non-collection properties
sonOfRa Nov 7, 2022
9a75ccb
Use Javers to build diffs
sonOfRa Nov 8, 2022
ffac4da
Properly handle basic realm attributes and clients
sonOfRa Nov 9, 2022
1ca6993
Note for deleted client handling
sonOfRa Nov 10, 2022
d1d2f3b
Export for (client)scopeMappings
sonOfRa Nov 24, 2022
972bea0
Properly differentiate import/export
sonOfRa Nov 24, 2022
49a6e69
Use similar file params to import
sonOfRa Nov 29, 2022
6e94f14
Export -> Normalization
sonOfRa Nov 30, 2022
f0e15a8
Normalize: Filter attributes
sonOfRa Dec 2, 2022
04ed90b
Fix checkstyle warnings
sonOfRa Dec 2, 2022
73b622d
Properly ignore some more configuration parameters
sonOfRa Dec 5, 2022
dc98239
Bump to Spring Boot 2.7.5
sonOfRa Dec 5, 2022
72e3c4f
Rename export profile to normalize
sonOfRa Dec 5, 2022
e041d08
Prepare for testing
sonOfRa Dec 5, 2022
3bc830d
Split up normalization into separate services
sonOfRa Dec 9, 2022
759fa1b
ClientScope normalization
sonOfRa Dec 12, 2022
105969d
Allow JSON export
sonOfRa Dec 12, 2022
4eade1f
Fix NPE in protocolMapper handling
sonOfRa Dec 12, 2022
5650f9b
Implement fallback-version parameter
sonOfRa Dec 12, 2022
0030cc0
Normalize roles
sonOfRa Dec 15, 2022
83b1621
Fix checkstyle
sonOfRa Dec 15, 2022
c0195f3
Group normalization
sonOfRa Dec 16, 2022
b1663df
Normalize AuthFlows and Configs
sonOfRa Dec 20, 2022
1d5cfe6
IdP and IdP Mapper normalization
sonOfRa Dec 20, 2022
9daaa0d
Normalize Required Actions
sonOfRa Dec 21, 2022
cf4c3ad
20.0.3 baseline
sonOfRa Feb 14, 2023
f494004
Update baseline representations
sonOfRa Feb 15, 2023
7a96074
Better handling for roles
sonOfRa Mar 8, 2023
93c5fbc
Fix incomplete required actions
sonOfRa Mar 9, 2023
f3282bf
Fix normalization bug with missing properties
sonOfRa Mar 10, 2023
8e4925b
Add normalization for User Federations
sonOfRa Mar 10, 2023
92325e5
Fix possible NPEs
sonOfRa Mar 13, 2023
741d49b
Fix UOE when using partial keycloak export
sonOfRa Apr 3, 2023
55710e7
Improved client normalization
sonOfRa Apr 3, 2023
5a28f89
Start work on ComponentNormalization
sonOfRa Apr 3, 2023
e40e3fb
Fix NPE when client attributes are not set
sonOfRa Apr 4, 2023
eb16660
Fix handling of invalid executions
sonOfRa Apr 4, 2023
9652d96
Remove technical IDs from authenticatorconfig
sonOfRa Apr 4, 2023
e8e889a
Map client authflow bindings to aliases
sonOfRa Apr 5, 2023
53a7e03
Tentative fix for unused authentication flows
sonOfRa Apr 5, 2023
38c5f23
Fix checkstyle line length error
sonOfRa Apr 5, 2023
b4e28ec
Documentation for detection of broken configs
sonOfRa Apr 19, 2023
debe1f0
Fix PMD warnings
sonOfRa Apr 19, 2023
5572c1e
Fix errors with duplicate mappers
sonOfRa Apr 20, 2023
64669da
Log names of unused flows
sonOfRa Apr 20, 2023
a4bb58c
Support clientpolicy and profile
sonOfRa Apr 20, 2023
87e968e
Always set publicClient attribute
sonOfRa May 26, 2023
fe9de52
Added some test for authFlows
Jun 5, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Expand Up @@ -19,3 +19,4 @@ release.properties

/*.json
/test*
/exports
1 change: 1 addition & 0 deletions docs/FEATURES.md
Expand Up @@ -55,6 +55,7 @@
| Remove clientScopeMappings | 2.5.0 | Remove existing clientScopeMappings while creating or updating realms |
| Synchronize user federation | 3.5.0 | Synchronize the user federation defined on the realm configuration |
| Synchronize user profile | 5.4.0 | Synchronize the user profile configuration defined on the realm configuration |
| Normalize realm exports | x.x.x | Normalize a full realm export to be more minimal |

# Specificities

Expand Down
123 changes: 123 additions & 0 deletions docs/NORMALIZE.md
@@ -0,0 +1,123 @@
# Realm normalization

Realm normalization is a feature that is supposed to aid users in migrating from an "unmanaged" Keycloak installation,
to an installation managed by keycloak-config-cli.
To achieve this, it uses a full [realm export](https://www.keycloak.org/server/importExport#_exporting_a_specific_realm)
as an input, and only retains things that deviate from the default

## Usage

To run the normalization, run keycloak-config-cli with the CLI option `--run.operation=NORMALIZE`.
The default value for this option is `IMPORT`, which will run the regular keycloak-config-cli import.

### Configuration options

| Configuration key | Purpose | Example |
|--------------------------------------|-----------------------------------------------------------------------------------------------------------------|---------------|
| run.operation | Tell keycloak-config-cli to normalize, rather than import | NORMALIZE |
| normalization.files.input-locations | Which realm files to import | See IMPORT.md |
| normalization.files.output-directory | Where to save output realm files | ./exports/out |
| normalization.output-format | Whether to output JSON or YAML. Default value is YAML | YAML |
| normalization.fallback-version | Use this version as a baseline of keycloak version in realm is not available as baseline in keycloak-config-cli | 19.0.3 |

### Unimplemented Features
- Components:
- Currently, keycloak-config-cli will not yet look at the `components` section of the exported JSON
- Therefore, some things (like LDAP federation configs and Key providers) are missing from the normalized YAML
- Users
- Users are not currently considered by normalization.

## Missing entries
keycloak-config-cli will WARN if components that are present in a realm by default are missing from an exported realm.
An example of such a message is:
```
Default realm requiredAction 'webauthn-register-passwordless' was deleted in exported realm. It may be reintroduced during import
```
Messages like these will often show up when using keycloak-config-cli to normalize an import from an older version of Keycloak, and compared to a newer baseline.
In the above case, the Keycloak version is 18.0.3, and the baseline for comparison was 19.0.3.
Since the webauthn feature was not present (or enabled by default) in the older version, this message is mostly informative.
If a message like this appears on a component that was *not* deleted or should generally be present, this may indicate a bug in keycloak-config-cli.

## Cleaning of invalid data
Sometimes, realms of existing installations may contain invalid data, due to faulty migrations, or due to direct interaction with the database,
rather than the Keycloak API.
When such problems are found, we attempt to handle them in keycloak-config-cli, or at least notify the user about the existence of these problems.

### SAML Attributes on clients
While not necessarily invalid, openid-connect clients that were created on older Keycloak versions, will sometimes contain
SAML-related attributes. These are filtered out by keycloak-config-cli.

### Unused non-top-level Authentication Flows
Authentication flows in Keycloak are marked as top-level if they are supposed to be available for binding or overrides.
Authentication flows that are not marked as top-level are used as sub-flows in other authentication flows.
The normalization process recognizes recursively whether there are authentication flows that are not top level and not used
by any top level flow, and does not include them in the final result.

A warning message is logged, and you can use the following SQL query to find any authentication flows that are not referenced.
Note that this query, unlike keycloak-config-cli, is not recursive.
That means that after deleting an unused flow, additional unused flows may appear after the query is performed again.

```sql
select flow.alias
from authentication_flow flow
join realm r on flow.realm_id = r.id
left join authentication_execution execution on flow.id = execution.auth_flow_id
where r.name = 'mytest'
and execution.id is null
and not flow.top_level
```

### Unused and duplicate Authenticator Configs
Authenticator Configs are not useful if they are not referenced by at least one authentication execution.
Therefore, keycloak-config-cli detects unused configurations and does not include them in the resulting output.
Note that the check for unused configs runs *after* the check for unused flows.
That means a config will be detected as unused if it is referenced by an execution that is part of a flow that is unused.

A warning message is logged on duplicate or unused configs, and you can use the following SQL query to find any configs
that are unused:

```sql
select ac.alias, ac.id
from authenticator_config ac
left join authentication_execution ae on ac.id = ae.auth_config
left join authentication_flow af on ae.flow_id = af.id
join realm r on ac.realm_id = r.id
where r.name = 'master' and af.alias is null
order by ac.alias
```

And the following query to find duplicates:

```sql
select alias, count(alias), r.name as realm_name
from authenticator_config
join realm r on realm_id = r.id
group by alias, r.name
having count(alias) > 1
```

If the `af.id` and `af.alias` fields are `null`, the config in question is not in use.
Note that configs used by unused flows are not marked as unused in the SQL result, as these need to be deleted first
to become unused.
After the unused flows (and executions) are deleted, the configs will be marked as unused and can also be deleted.

### Authentication Executions with invalid subflows
Some keycloak exports have invalid authentication executions that reference a subflow, while also setting an authenticator.
This is only a valid configuration if the subflow's type is `form-flow`.
If it is not, then keycloak-config-cli will not import the configuration.
This will be marked by an ERROR severity message in the log output.
You can use this SQL query to find offending entries and remediate the configuration errors before continuing.

```sql
select parent.alias,
subflow.alias,
execution.alias
from authentication_execution execution
join realm r on execution.realm_id = r.id
join authentication_flow parent on execution.flow_id = parent.id
join authentication_flow subflow on execution.auth_flow_id = subflow.id
where execution.auth_flow_id is not null
and execution.authenticator is not null
and subflow.provider_id <> 'form-flow'
and r.name = 'REALMNAME';
```
18 changes: 17 additions & 1 deletion pom.xml
Expand Up @@ -59,7 +59,7 @@
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>

<keycloak.version>21.0.1</keycloak.version>
<keycloak.version>18.0.2</keycloak.version>

<checkstyle-plugin.version>3.2.0</checkstyle-plugin.version>
<checkstyle.version>10.0</checkstyle.version>
Expand Down Expand Up @@ -153,6 +153,12 @@
<artifactId>failsafe</artifactId>
<version>${failsafe.version}</version>
</dependency>

<dependency>
<groupId>org.javers</groupId>
<artifactId>javers-core</artifactId>
<version>6.8.0</version>
</dependency>
</dependencies>
</dependencyManagement>

Expand Down Expand Up @@ -230,6 +236,16 @@
<artifactId>jackson-databind</artifactId>
</dependency>

<dependency>
<groupId>com.fasterxml.jackson.dataformat</groupId>
<artifactId>jackson-dataformat-yaml</artifactId>
</dependency>

<dependency>
<groupId>org.javers</groupId>
<artifactId>javers-core</artifactId>
</dependency>

<dependency>
<groupId>org.yaml</groupId>
<artifactId>snakeyaml</artifactId>
Expand Down
Expand Up @@ -20,14 +20,13 @@

package de.adorsys.keycloak.config;

import de.adorsys.keycloak.config.properties.ImportConfigProperties;
import de.adorsys.keycloak.config.properties.KeycloakConfigProperties;
import de.adorsys.keycloak.config.properties.RunConfigProperties;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.properties.EnableConfigurationProperties;

@SpringBootApplication(proxyBeanMethods = false)
@EnableConfigurationProperties({KeycloakConfigProperties.class, ImportConfigProperties.class})
@EnableConfigurationProperties(RunConfigProperties.class)
public class KeycloakConfigApplication {
public static void main(String[] args) {
// https://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/#boot-features-application-exit
Expand Down
@@ -0,0 +1,126 @@
/*-
* ---license-start
* keycloak-config-cli
* ---
* Copyright (C) 2017 - 2022 adorsys GmbH & Co. KG @ https://adorsys.com
* ---
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* ---license-end
*/

package de.adorsys.keycloak.config;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.dataformat.yaml.YAMLMapper;
import de.adorsys.keycloak.config.properties.NormalizationConfigProperties;
import de.adorsys.keycloak.config.properties.NormalizationKeycloakConfigProperties;
import de.adorsys.keycloak.config.provider.KeycloakExportProvider;
import de.adorsys.keycloak.config.service.normalize.RealmNormalizationService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.ExitCodeGenerator;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.stereotype.Component;

import java.io.FileOutputStream;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.text.SimpleDateFormat;
import java.util.Date;

import static de.adorsys.keycloak.config.properties.NormalizationConfigProperties.OutputFormat.YAML;

@Component
@ConditionalOnProperty(prefix = "run", name = "operation", havingValue = "NORMALIZE")
@EnableConfigurationProperties({NormalizationConfigProperties.class, NormalizationKeycloakConfigProperties.class})
public class KeycloakConfigNormalizationRunner implements CommandLineRunner, ExitCodeGenerator {

private static final Logger logger = LoggerFactory.getLogger(KeycloakConfigNormalizationRunner.class);
private static final long START_TIME = System.currentTimeMillis();

private final RealmNormalizationService normalizationService;
private final KeycloakExportProvider exportProvider;
private final NormalizationConfigProperties normalizationConfigProperties;
private final YAMLMapper yamlMapper;
private final ObjectMapper objectMapper;
private int exitCode;

@Autowired
public KeycloakConfigNormalizationRunner(RealmNormalizationService normalizationService,
KeycloakExportProvider exportProvider,
NormalizationConfigProperties normalizationConfigProperties,
YAMLMapper yamlMapper,
ObjectMapper objectMapper) {
this.normalizationService = normalizationService;
this.exportProvider = exportProvider;
this.normalizationConfigProperties = normalizationConfigProperties;
this.yamlMapper = yamlMapper;
this.objectMapper = objectMapper;
}

@Override
public void run(String... args) throws Exception {
try {
var outputLocation = Paths.get(normalizationConfigProperties.getFiles().getOutputDirectory());
if (!Files.exists(outputLocation)) {
logger.info("Creating output directory '{}'", outputLocation);
Files.createDirectories(outputLocation);
}
if (!Files.isDirectory(outputLocation)) {
logger.error("Output location '{}' is not a directory. Aborting", outputLocation);
exitCode = 1;
return;
}

for (var exportLocations : exportProvider.readFromLocations().values()) {
for (var export : exportLocations.entrySet()) {
logger.info("Normalizing file '{}'", export.getKey());
for (var realm : export.getValue()) {
var normalizedRealm = normalizationService.normalizeRealm(realm);
var suffix = normalizationConfigProperties.getOutputFormat() == YAML ? "yaml" : "json";
var outputFile = outputLocation.resolve(String.format("%s.%s", normalizedRealm.getRealm(), suffix));
try (var os = new FileOutputStream(outputFile.toFile())) {
if (normalizationConfigProperties.getOutputFormat() == YAML) {
yamlMapper.writeValue(os, normalizedRealm);
} else {
objectMapper.writeValue(os, normalizedRealm);
}
}
}
}
}
} catch (NullPointerException e) {
throw e;
} catch (Exception e) {
logger.error(e.getMessage());

exitCode = 1;

if (logger.isDebugEnabled()) {
throw e;
}
} finally {
long totalTime = System.currentTimeMillis() - START_TIME;
String formattedTime = new SimpleDateFormat("mm:ss.SSS").format(new Date(totalTime));
logger.info("keycloak-config-cli running in {}.", formattedTime);
}
}

@Override
public int getExitCode() {
return exitCode;
}
}
10 changes: 10 additions & 0 deletions src/main/java/de/adorsys/keycloak/config/KeycloakConfigRunner.java
Expand Up @@ -23,13 +23,16 @@
import de.adorsys.keycloak.config.model.KeycloakImport;
import de.adorsys.keycloak.config.model.RealmImport;
import de.adorsys.keycloak.config.properties.ImportConfigProperties;
import de.adorsys.keycloak.config.properties.KeycloakConfigProperties;
import de.adorsys.keycloak.config.provider.KeycloakImportProvider;
import de.adorsys.keycloak.config.service.RealmImportService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.ExitCodeGenerator;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.stereotype.Component;

import java.text.SimpleDateFormat;
Expand All @@ -39,6 +42,13 @@
import java.util.Map;

@Component
/*
* Spring only considers actual properties set, not default values of @ConfigurationProperties classes.
* Therefore, we enable matchIfMissing here, so if there is *no* property set, we consider it an import
* for backwards compatibility
*/
@ConditionalOnProperty(prefix = "run", name = "operation", havingValue = "IMPORT", matchIfMissing = true)
@EnableConfigurationProperties({ImportConfigProperties.class, KeycloakConfigProperties.class})
public class KeycloakConfigRunner implements CommandLineRunner, ExitCodeGenerator {
private static final Logger logger = LoggerFactory.getLogger(KeycloakConfigRunner.class);
private static final long START_TIME = System.currentTimeMillis();
Expand Down