Skip to content

Commit

Permalink
Manage components and subComponents
Browse files Browse the repository at this point in the history
  • Loading branch information
jkroepke committed Jun 12, 2020
1 parent 16b3f6d commit 6818c0a
Show file tree
Hide file tree
Showing 23 changed files with 1,562 additions and 95 deletions.
4 changes: 3 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- `import.file` is removed. Use `import.path` instead for files and directories.
- `keycloak.migrationKey` is removed. Use `import.cache-key` instead.
- `keycloak.realm` is removed. Use `import.login-realm` to define the realm to login.
- If you have defined requiredActions in your realm configure, make sure you have defined all in your json files. All not defined
- If you have defined requiredActions, components or subcomponents in your realm configure, make sure you have defined all in your json files. All not defined
actions will removed now by keycloak-config-cli. See: [docs/MANAGED.md](docs/MANAGED.md)

### Added
Expand All @@ -22,6 +22,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Throw errors on unknown properties in config files
- Add, update and remove clientScopes
- Removed required actions if they not defined in import json.
- Removed components if they not defined in import json.
- Removed subcomponents if they not defined in import json.
- Contrib behaivor of purging ressource via `import.manage.<type>` property. See: [docs/MANAGED.md](docs/MANAGED.md)

### Changed
Expand Down
6 changes: 4 additions & 2 deletions docs/FEATURES.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@

| Feature | Since | Description |
| ------------------------------------------ | ----- | -------------------------------------------------------------------------------------------------------- |
| Create clients | 1.0.0 | Create client configuration (inclusive protocolMappers) while creating or updating realms |
| Update clients | 1.0.0 | Update client configuration (inclusive protocolMappers) while updating realms |
| Create clients | 1.0.0 | Create client configuration (inclusive protocolMappers) while creating or updating realms |
| Update clients | 1.0.0 | Update client configuration (inclusive protocolMappers) while updating realms |
| Add roles | 1.0.0 | Add roles while creating or updating realms |
| Update roles | 1.0.0 | Update role properties while updating realms |
| Add composites to roles | 1.3.0 | Add role with realm-level and client-level composite roles while creating or updating realms |
Expand All @@ -18,7 +18,9 @@
| Update authentication flows and executions | 1.0.0 | Update authentication flow properties and executions while updating realms |
| Add components | 1.0.0 | Add components while creating or updating realms |
| Update components | 1.0.0 | Update components properties while updating realms |
| Remove components | 2.0.0 | Remove existing sub-components while creating or updating realms |
| Update sub-components | 1.0.0 | Add sub-components properties while creating or updating realms |
| Remove sub-components | 2.0.0 | Remove existing sub-components while creating or updating realms |
| Add groups | 1.3.0 | Add groups (inclusive subgroups!) to realm while creating or updating realms |
| Update groups | 1.3.0 | Update existing group properties and attributes while creating or updating realms |
| Remove groups | 1.3.0 | Remove existing groups while updating realms |
Expand Down
14 changes: 8 additions & 6 deletions docs/MANAGED.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,14 @@ For example if you define `groups` but set an empty array, keycloak will delete

## Supported full managed entities

| Type | Additional Information |
| ---------------- | ----------------------------------------------------- |
| Groups | - |
| Required Actions | You have to copy the default one to you import json. |
| Client Scopes | - |
| Scope Mappings | - |
| Type | Additional Information |
| ---------------- | ----------------------------------------------------------- |
| Groups | - |
| Required Actions | You have to copy the default one to you import json. |
| Client Scopes | - |
| Scope Mappings | - |
| Components | You have to copy the default components to you import json. |
| Sub Components | You have to copy the default components to you import json. |

## Disable deletion of managed entities

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -78,11 +78,23 @@ public static class ImportManagedProperties {
@NotNull
private final ImportManagedPropertiesValues scopeMapping;

private ImportManagedProperties(ImportManagedPropertiesValues group, ImportManagedPropertiesValues requiredAction, ImportManagedPropertiesValues clientScope, @NotNull ImportManagedPropertiesValues scopeMapping) {
@NotNull
private final ImportManagedPropertiesValues component;

@NotNull
private final ImportManagedPropertiesValues subComponent;

private ImportManagedProperties(
ImportManagedPropertiesValues group, ImportManagedPropertiesValues requiredAction,
ImportManagedPropertiesValues clientScope, ImportManagedPropertiesValues scopeMapping,
ImportManagedPropertiesValues component, ImportManagedPropertiesValues subComponent
) {
this.group = group;
this.requiredAction = requiredAction;
this.clientScope = clientScope;
this.scopeMapping = scopeMapping;
this.component = component;
this.subComponent = subComponent;
}

public ImportManagedPropertiesValues getGroup() {
Expand All @@ -101,6 +113,14 @@ public ImportManagedPropertiesValues getScopeMapping() {
return scopeMapping;
}

public ImportManagedPropertiesValues getComponent() {
return component;
}

public ImportManagedPropertiesValues getSubComponent() {
return subComponent;
}

public enum ImportManagedPropertiesValues {
full,
noDelete
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
import org.springframework.stereotype.Service;

import javax.ws.rs.core.Response;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
Expand All @@ -41,52 +42,68 @@ public ComponentRepository(RealmRepository realmRepository) {
this.realmRepository = realmRepository;
}

public void create(String realm, ComponentRepresentation componentToCreate) {
public void create(String realm, ComponentRepresentation component) {
RealmResource realmResource = realmRepository.loadRealm(realm);
Response response = realmResource.components().add(componentToCreate);
Response response = realmResource.components().add(component);

ResponseUtil.throwOnError(response);
}

public void update(String realm, ComponentRepresentation componentToUpdate) {
public void update(String realm, ComponentRepresentation component) {
RealmResource realmResource = realmRepository.loadRealm(realm);
ComponentResource componentResource = realmResource.components().component(componentToUpdate.getId());
ComponentResource componentResource = realmResource.components().component(component.getId());

componentResource.update(componentToUpdate);
componentResource.update(component);
}

public ComponentRepresentation get(String realm, String subType, String name) {
public void delete(String realm, ComponentRepresentation component) {
RealmResource realmResource = realmRepository.loadRealm(realm);
ComponentResource componentResource = realmResource.components().component(component.getId());

componentResource.remove();
}

public ComponentRepresentation get(String realm, String providerType, String name) {
RealmResource realmResource = realmRepository.loadRealm(realm);

List<ComponentRepresentation> realmComponents = realmResource.components().query();

Optional<ComponentRepresentation> maybeComponent = realmComponents
.stream()
.filter(c -> Objects.equals(c.getName(), name))
.filter(c -> Objects.equals(c.getProviderType(), subType))
.filter(c -> Objects.equals(c.getProviderType(), providerType))
.findFirst();

if (maybeComponent.isPresent()) {
return maybeComponent.get();
}

throw new KeycloakRepositoryException("Cannot find component by name '" + name + "' and subtype '" + subType + "' in realm '" + realm + "' ");
throw new KeycloakRepositoryException("Cannot find component by name '" + name + "' and subtype '" + providerType + "' in realm '" + realm + "' ");
}

public Optional<ComponentRepresentation> tryToGet(String realm, String parentId, String subType, String name) {
public List<ComponentRepresentation> getAllComponents(String realm) {
RealmResource realmResource = realmRepository.loadRealm(realm);
String realmId = realmResource.toRepresentation().getId();

Optional<ComponentRepresentation> maybeComponent;
List<ComponentRepresentation> existingComponents = realmResource.components()
.query(parentId, subType, name);
List<ComponentRepresentation> subComponents = realmResource.components().query(realmId);

if (existingComponents.isEmpty()) {
maybeComponent = Optional.empty();
} else {
maybeComponent = Optional.of(existingComponents.get(0));
if (subComponents == null) {
return Collections.emptyList();
}

return maybeComponent;
return subComponents;
}

public List<ComponentRepresentation> getAllSubComponentsByParentId(String realm, String parentId) {
RealmResource realmResource = realmRepository.loadRealm(realm);

List<ComponentRepresentation> subComponents = realmResource.components().query(parentId);

if (subComponents == null) {
return Collections.emptyList();
}

return subComponents;
}

/**
Expand All @@ -101,9 +118,24 @@ public Optional<ComponentRepresentation> tryToGetComponent(String realm, String
.query();

return existingComponents.stream()
.filter(c -> c.getName().equals(name))
.filter(c -> Objects.equals(c.getName(), name))
.filter(c -> Objects.equals(c.getSubType(), subType))
.findFirst();
}

public Optional<ComponentRepresentation> tryToGetSubComponent(String realm, String parentId, String subType, String name) {
RealmResource realmResource = realmRepository.loadRealm(realm);

Optional<ComponentRepresentation> maybeComponent;
List<ComponentRepresentation> existingComponents = realmResource.components()
.query(parentId, subType, name);

if (existingComponents.isEmpty()) {
maybeComponent = Optional.empty();
} else {
maybeComponent = Optional.of(existingComponents.get(0));
}

return maybeComponent;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@
import de.adorsys.keycloak.config.exception.ImportProcessingException;
import de.adorsys.keycloak.config.exception.KeycloakRepositoryException;
import de.adorsys.keycloak.config.model.RealmImport;
import de.adorsys.keycloak.config.properties.ImportConfigProperties;
import de.adorsys.keycloak.config.properties.ImportConfigProperties.ImportManagedProperties.ImportManagedPropertiesValues;
import de.adorsys.keycloak.config.repository.ComponentRepository;
import de.adorsys.keycloak.config.util.CloneUtil;
import org.keycloak.common.util.MultivaluedHashMap;
Expand All @@ -40,10 +42,12 @@ public class ComponentImportService {
private static final Logger logger = LoggerFactory.getLogger(ComponentImportService.class);

private final ComponentRepository componentRepository;
private final ImportConfigProperties importConfigProperties;

@Autowired
public ComponentImportService(ComponentRepository componentRepository) {
public ComponentImportService(ComponentRepository componentRepository, ImportConfigProperties importConfigProperties) {
this.componentRepository = componentRepository;
this.importConfigProperties = importConfigProperties;
}

public void doImport(RealmImport realmImport) {
Expand All @@ -53,10 +57,14 @@ public void doImport(RealmImport realmImport) {
return;
}

createOrUpdateComponents(realmImport.getRealm(), components);
importComponents(realmImport.getRealm(), components);

if (importConfigProperties.getManaged().getComponent() == ImportManagedPropertiesValues.full) {
deleteComponentsMissingInImport(realmImport.getRealm(), components);
}
}

private void createOrUpdateComponents(String realm, Map<String, List<ComponentExportRepresentation>> componentsToImport) {
private void importComponents(String realm, Map<String, List<ComponentExportRepresentation>> componentsToImport) {
for (Map.Entry<String, List<ComponentExportRepresentation>> entry : componentsToImport.entrySet()) {
createOrUpdateComponents(realm, entry.getKey(), entry.getValue());
}
Expand Down Expand Up @@ -111,10 +119,19 @@ private void createComponent(

MultivaluedHashMap<String, ComponentExportRepresentation> subComponents = component.getSubComponents();

if (subComponents != null && !subComponents.isEmpty()) {
ComponentRepresentation exitingComponent = componentRepository.get(realm, providerType, component.getName());
if (subComponents == null) {
return;
}

ComponentRepresentation exitingComponent = componentRepository.get(realm, providerType, component.getName());

if (!subComponents.isEmpty()) {
createOrUpdateSubComponents(realm, subComponents, exitingComponent.getId());
}

if (importConfigProperties.getManaged().getComponent() == ImportManagedPropertiesValues.full) {
deleteSubComponentsMissingInImport(realm, subComponents, exitingComponent.getId());
}
}

private void updateComponentIfNeeded(
Expand Down Expand Up @@ -150,8 +167,14 @@ private void updateComponent(

MultivaluedHashMap<String, ComponentExportRepresentation> subComponents = componentToImport.getSubComponents();

if (subComponents != null && !subComponents.isEmpty()) {
createOrUpdateSubComponents(realm, subComponents, patchedComponent.getId());
if (subComponents != null) {
if (!subComponents.isEmpty()) {
createOrUpdateSubComponents(realm, subComponents, patchedComponent.getId());
}

if (importConfigProperties.getManaged().getSubComponent() == ImportManagedPropertiesValues.full) {
deleteSubComponentsMissingInImport(realm, subComponents, patchedComponent.getId());
}
}
}

Expand All @@ -168,12 +191,56 @@ private void createOrUpdateSubComponents(String realm, String providerType, List
}

private void createOrUpdateSubComponent(String realm, String parentId, String providerType, ComponentExportRepresentation subComponent) {
Optional<ComponentRepresentation> maybeComponent = componentRepository.tryToGet(realm, parentId, subComponent.getSubType(), subComponent.getName());
Optional<ComponentRepresentation> maybeComponent = componentRepository.tryToGetSubComponent(realm, parentId, subComponent.getSubType(), subComponent.getName());

if (maybeComponent.isPresent()) {
updateComponentIfNeeded(realm, providerType, subComponent, maybeComponent.get());
} else {
createComponent(realm, providerType, subComponent, parentId);
}
}

private void deleteComponentsMissingInImport(String realm, MultivaluedHashMap<String, ComponentExportRepresentation> componentsToImport) {
List<ComponentRepresentation> existingComponents = componentRepository.getAllComponents(realm);

for (ComponentRepresentation existingComponent : existingComponents) {
if (checkIfComponentMissingImport(existingComponent, componentsToImport)) {
logger.debug("Delete component: {}/{}", existingComponent.getProviderType(), existingComponent.getName());
componentRepository.delete(realm, existingComponent);
}
}
}

private void deleteSubComponentsMissingInImport(String realm, MultivaluedHashMap<String, ComponentExportRepresentation> subComponentsToImport, String parentId) {
List<ComponentRepresentation> existingSubComponents = componentRepository.getAllSubComponentsByParentId(realm, parentId);

for (ComponentRepresentation existingSubComponent : existingSubComponents) {
if (checkIfComponentMissingImport(existingSubComponent, subComponentsToImport)) {
logger.debug("Delete component: {}/{}", existingSubComponent.getProviderType(), existingSubComponent.getName());
componentRepository.delete(realm, existingSubComponent);
}
}
}

private boolean checkIfComponentMissingImport(ComponentRepresentation existingComponent, MultivaluedHashMap<String, ComponentExportRepresentation> componentsToImport) {
String existingComponentProviderType = existingComponent.getProviderType();
String existingComponentName = existingComponent.getName();

for (Map.Entry<String, List<ComponentExportRepresentation>> entry : componentsToImport.entrySet()) {
String providerType = entry.getKey();
List<ComponentExportRepresentation> componentToImport = entry.getValue();

if (!existingComponentProviderType.equals(providerType)) {
continue;
}

boolean isInImport = componentToImport.stream().anyMatch((component) -> existingComponentName.equals(component.getName()));

if (isInImport) {
return false;
}
}

return true;
}
}
2 changes: 2 additions & 0 deletions src/main/resources/application.properties
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,5 @@ import.managed.group=full
import.managed.required-action=full
import.managed.client-scope=full
import.managed.scope-mapping=full
import.managed.component=nodelete
import.managed.sub-component=full
Loading

0 comments on commit 6818c0a

Please sign in to comment.