Skip to content

Commit

Permalink
#280 Add support for shared IDPs import/export (#285)
Browse files Browse the repository at this point in the history
  • Loading branch information
rtufisi authored Dec 10, 2024
1 parent b861e5a commit 51a55b9
Show file tree
Hide file tree
Showing 4 changed files with 282 additions and 8 deletions.
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
package io.phasetwo.service.importexport;

import static io.phasetwo.service.Orgs.ORG_OWNER_CONFIG_KEY;

import io.phasetwo.service.importexport.representation.OrganizationAttributes;
import io.phasetwo.service.importexport.representation.OrganizationRepresentation;
import io.phasetwo.service.importexport.representation.OrganizationRoleRepresentation;
Expand All @@ -13,12 +11,20 @@
import java.util.List;
import java.util.Objects;
import java.util.Set;

import lombok.extern.jbosslog.JBossLog;
import org.keycloak.models.IdentityProviderModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.ModelException;
import org.keycloak.models.RealmModel;
import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.models.utils.ModelToRepresentation;
import org.keycloak.models.utils.RepresentationToModel;
import org.keycloak.representations.idm.IdentityProviderRepresentation;

import static io.phasetwo.service.Orgs.ORG_CONFIG_SHARED_IDPS_KEY;
import static io.phasetwo.service.Orgs.ORG_OWNER_CONFIG_KEY;
import static io.phasetwo.service.Orgs.ORG_SHARED_IDP_KEY;

@JBossLog
public final class KeycloakOrgsImportConverter {
Expand Down Expand Up @@ -109,13 +115,15 @@ public static void addMembers(
}

public static void createOrganizationIdp(
KeycloakSession session, String idpLink, OrganizationModel org, boolean skipMissingIdp) {
KeycloakSession session, RealmModel realm, String idpLink, OrganizationModel org, boolean skipMissingIdp) {
if (Objects.nonNull(idpLink)) {
IdentityProviderModel idp = session.identityProviders().getByAlias(idpLink);
if (Objects.nonNull(idp)) {
IdentityProviders.setAttributeMultivalued(
idp.getConfig(), ORG_OWNER_CONFIG_KEY, Set.of(org.getId()));
session.identityProviders().update(idp);
IdentityProviderRepresentation representation = ModelToRepresentation.toRepresentation(realm, idp);
processsSharedIdps(realm, org, representation);

IdentityProviderModel updated = RepresentationToModel.toModel(realm, representation, session);
session.identityProviders().update(updated);
} else {
if (skipMissingIdp) {
log.debug(
Expand All @@ -129,7 +137,19 @@ public static void createOrganizationIdp(
}
}

public static void createOrganizationRoles(
private static void processsSharedIdps(RealmModel realm, OrganizationModel org, IdentityProviderRepresentation representation) {
var isSharedIdpsConfigEnabled = realm.getAttribute(ORG_CONFIG_SHARED_IDPS_KEY, false);

if (isSharedIdpsConfigEnabled) {
IdentityProviders.addMultiOrganization(org, representation);
} else {
representation.getConfig().put(ORG_SHARED_IDP_KEY, "false");
IdentityProviders.setAttributeMultivalued(
representation.getConfig(), ORG_OWNER_CONFIG_KEY, Set.of(org.getId()));
}
}

public static void createOrganizationRoles(
List<OrganizationRoleRepresentation> roles, OrganizationModel org) {
roles.stream()
.filter(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -314,7 +314,7 @@ private void createOrganization(
KeycloakOrgsImportConverter.createOrganizationRoles(
organizationRepresentation.getRoles(), org);

KeycloakOrgsImportConverter.createOrganizationIdp(session, organizationRepresentation.getIdpLink(), org, skipMissingIdp);
KeycloakOrgsImportConverter.createOrganizationIdp(session, realm, organizationRepresentation.getIdpLink(), org, skipMissingIdp);

KeycloakOrgsImportConverter.addMembers(
session, realm, organizationRepresentation, org, skipMissingMember);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,21 @@
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.hasSize;
import static org.hamcrest.Matchers.is;
import static org.junit.Assert.assertTrue;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.google.common.collect.ImmutableMap;
import io.phasetwo.client.openapi.model.OrganizationRepresentation;
import io.phasetwo.service.AbstractOrganizationTest;
import io.phasetwo.service.representation.LinkIdp;
import java.io.IOException;
import java.util.List;

import io.phasetwo.service.representation.OrganizationsConfig;
import lombok.extern.jbosslog.JBossLog;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

@JBossLog
Expand Down Expand Up @@ -71,4 +80,115 @@ void testOrganizationLinkIdp() throws IOException {
// delete idp
keycloak.realm(REALM).identityProviders().get(alias1).remove();
}

@Test
void testOrganizationChangeLink() throws IOException {
// create organization
var organization1 =
createOrganization(
new OrganizationRepresentation()
.name("example-org")
.domains(List.of("example.com", "test.org")));
// create organization
var organization2 =
createOrganization(
new OrganizationRepresentation()
.name("example-org2")
.domains(List.of("example2.com", "test2.org")));

// create identity provider
String alias1 = "linking-provider-1";
org.keycloak.representations.idm.IdentityProviderRepresentation idp =
new org.keycloak.representations.idm.IdentityProviderRepresentation();
idp.setAlias(alias1);
idp.setProviderId("oidc");
idp.setEnabled(true);
idp.setFirstBrokerLoginFlowAlias("first broker login");
idp.setConfig(
new ImmutableMap.Builder<String, String>()
.put("useJwksUrl", "true")
.put("syncMode", "FORCE")
.put("authorizationUrl", "https://foo.com")
.put("hideOnLoginPage", "")
.put("loginHint", "")
.put("uiLocales", "")
.put("backchannelSupported", "")
.put("disableUserInfo", "")
.put("acceptsPromptNoneForwardFromClient", "")
.put("validateSignature", "")
.put("pkceEnabled", "")
.put("tokenUrl", "https://foo.com")
.put("clientAuthMethod", "client_secret_post")
.put("clientId", "aabbcc")
.put("clientSecret", "112233")
.build());
keycloak.realm(REALM).identityProviders().create(idp);

// link org1
LinkIdp link1 = new LinkIdp();
link1.setAlias(alias1);
link1.setSyncMode("IMPORT");
var responseOrg1Link = postRequest(link1, organization1.getId(), "idps", "link");
assertThat(
responseOrg1Link.getStatusCode(),
is(jakarta.ws.rs.core.Response.Status.CREATED.getStatusCode()));

// link org2
var link2 = new LinkIdp();
link2.setAlias(alias1);
link2.setSyncMode("IMPORT");
var responseOrg2Link = postRequest(link2, organization2.getId(), "idps", "link");
assertThat(
responseOrg2Link.getStatusCode(),
is(jakarta.ws.rs.core.Response.Status.CREATED.getStatusCode()));

// export
var export = exportOrgs(keycloak, true);
assertThat(export.getOrganizations(), hasSize(2));

// validate org1
export.getOrganizations().stream()
.filter(exportOrg -> exportOrg.getOrganization().getName().equals(organization1.getName()))
.findFirst()
.ifPresent(exportOrg -> Assertions.assertNull(exportOrg.getIdpLink()));

// validate org2
export.getOrganizations().stream()
.filter(exportOrg -> exportOrg.getOrganization().getName().equals(organization2.getName()))
.findFirst()
.ifPresent(exportOrg -> assertThat(exportOrg.getIdpLink(), is(alias1)));

// delete org1
deleteOrganization(organization1.getId());

// delete org2
deleteOrganization(organization2.getId());

// delete idp
keycloak.realm(REALM).identityProviders().get(alias1).remove();
}

@BeforeEach
public void beforeEach() throws JsonProcessingException {
// add shared idp master config
var url = getAuthUrl() + "/realms/master/orgs/config";
var orgConfig = new OrganizationsConfig();
orgConfig.setSharedIdps(false);
var responseOrgsConfig = putRequest(orgConfig, url);
assertThat(
responseOrgsConfig.getStatusCode(),
is(jakarta.ws.rs.core.Response.Status.OK.getStatusCode()));
}

@AfterEach
public void afterEach() throws JsonProcessingException {
// remove shared idp master config
var url = getAuthUrl() + "/realms/master/orgs/config";
var orgConfig = new OrganizationsConfig();
orgConfig.setSharedIdps(false);
var responseOrgsConfig = putRequest(orgConfig, url);
assertThat(
responseOrgsConfig.getStatusCode(),
is(jakarta.ws.rs.core.Response.Status.OK.getStatusCode()));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
package io.phasetwo.service.importexport;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.google.common.collect.ImmutableMap;
import io.phasetwo.client.openapi.model.OrganizationRepresentation;
import io.phasetwo.service.AbstractOrganizationTest;
import io.phasetwo.service.representation.LinkIdp;
import io.phasetwo.service.representation.OrganizationsConfig;
import lombok.extern.jbosslog.JBossLog;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

import java.io.IOException;
import java.util.List;

import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.hasSize;
import static org.hamcrest.Matchers.is;

@JBossLog
public class OrganizationSharedIdpExportTest extends AbstractOrganizationTest {

@Test
void testMultiOrganizationLink() throws IOException {
// create organization
var organization1 =
createOrganization(
new OrganizationRepresentation()
.name("example-org")
.domains(List.of("example.com", "test.org")));
// create organization
var organization2 =
createOrganization(
new OrganizationRepresentation()
.name("example-org2")
.domains(List.of("example2.com", "test2.org")));

// create identity provider
String alias1 = "linking-provider-1";
org.keycloak.representations.idm.IdentityProviderRepresentation idp =
new org.keycloak.representations.idm.IdentityProviderRepresentation();
idp.setAlias(alias1);
idp.setProviderId("oidc");
idp.setEnabled(true);
idp.setFirstBrokerLoginFlowAlias("first broker login");
idp.setConfig(
new ImmutableMap.Builder<String, String>()
.put("useJwksUrl", "true")
.put("syncMode", "FORCE")
.put("authorizationUrl", "https://foo.com")
.put("hideOnLoginPage", "")
.put("loginHint", "")
.put("uiLocales", "")
.put("backchannelSupported", "")
.put("disableUserInfo", "")
.put("acceptsPromptNoneForwardFromClient", "")
.put("validateSignature", "")
.put("pkceEnabled", "")
.put("tokenUrl", "https://foo.com")
.put("clientAuthMethod", "client_secret_post")
.put("clientId", "aabbcc")
.put("clientSecret", "112233")
.build());
keycloak.realm(REALM).identityProviders().create(idp);

// link org1
LinkIdp link1 = new LinkIdp();
link1.setAlias(alias1);
link1.setSyncMode("IMPORT");
var responseOrg1Link = postRequest(link1, organization1.getId(), "idps", "link");
assertThat(
responseOrg1Link.getStatusCode(),
is(jakarta.ws.rs.core.Response.Status.CREATED.getStatusCode()));

// link org2
var link2 = new LinkIdp();
link2.setAlias(alias1);
link2.setSyncMode("IMPORT");
var responseOrg2Link = postRequest(link2, organization2.getId(), "idps", "link");
assertThat(
responseOrg2Link.getStatusCode(),
is(jakarta.ws.rs.core.Response.Status.CREATED.getStatusCode()));

// export
var export = exportOrgs(keycloak, true);
assertThat(export.getOrganizations(), hasSize(2));

// validate org1
export.getOrganizations().stream()
.filter(exportOrg -> exportOrg.getOrganization().getName().equals(organization1.getName()))
.findFirst()
.ifPresent(exportOrg -> assertThat(exportOrg.getIdpLink(), is(alias1)));

// validate org2
export.getOrganizations().stream()
.filter(exportOrg -> exportOrg.getOrganization().getName().equals(organization2.getName()))
.findFirst()
.ifPresent(exportOrg -> assertThat(exportOrg.getIdpLink(), is(alias1)));

// delete org1
deleteOrganization(organization1.getId());

// delete org2
deleteOrganization(organization2.getId());

// delete idp
keycloak.realm(REALM).identityProviders().get(alias1).remove();
}

@BeforeEach
public void beforeEach() throws JsonProcessingException {
// add shared idp master config
var url = getAuthUrl() + "/realms/master/orgs/config";
var orgConfig = new OrganizationsConfig();
orgConfig.setSharedIdps(true);
var responseOrgsConfig = putRequest(orgConfig, url);
assertThat(
responseOrgsConfig.getStatusCode(),
is(jakarta.ws.rs.core.Response.Status.OK.getStatusCode()));
}

@AfterEach
public void afterEach() throws JsonProcessingException {
// remove shared idp master config
var url = getAuthUrl() + "/realms/master/orgs/config";
var orgConfig = new OrganizationsConfig();
orgConfig.setSharedIdps(true);
var responseOrgsConfig = putRequest(orgConfig, url);
assertThat(
responseOrgsConfig.getStatusCode(),
is(jakarta.ws.rs.core.Response.Status.OK.getStatusCode()));
}
}

0 comments on commit 51a55b9

Please sign in to comment.