Adding an alias to organization and exposing them to templates

Closes #30312
Closes #30313

Signed-off-by: Pedro Igor <pigor.craveiro@gmail.com>
This commit is contained in:
Pedro Igor 2024-06-10 18:29:08 -03:00
parent 8d0e03a271
commit a0ad680346
36 changed files with 862 additions and 33 deletions

View file

@ -29,6 +29,7 @@ public class OrganizationRepresentation {
private String id;
private String name;
private String alias;
private boolean enabled = true;
private String description;
private Map<String, List<String>> attributes;
@ -52,6 +53,14 @@ public class OrganizationRepresentation {
return name;
}
public String getAlias() {
return alias;
}
public void setAlias(String alias) {
this.alias = alias;
}
public boolean isEnabled() {
return this.enabled;
}

View file

@ -30,6 +30,10 @@ An organization has the following settings:
Name::
A user-friendly name for the organization. The name is unique within a realm.
Alias::
An alias for this organization, used to reference the organization internally. The alias is unique within a realm.
If not set, the value is the same as the organization name. The alias cannot change afterwards.
Domains::
A set of one or more domains that belongs to this organization. A domain cannot be shared by different organizations
within a realm.

View file

@ -3164,6 +3164,7 @@ createOrganization=Create organization
domain=Domain
organizationDomainHelp=A set of one or more internet domains associated with the organization. The domain is used to map users to an organization based on their email domain and to authenticate them accordingly in the scope of the organization.
addDomain=Add domain
organizationAliasHelp=The alias uniquely identifies an organization using a format that is mainly targeted for referencing the organization internally. For instance, when issuing organization-related claims into tokens or when in a custom theme.
disableConfirmOrganizationTitle=Disable organization?
disableConfirmOrganization=Are you sure you want to disable this organization?
memberList=Member list

View file

@ -9,6 +9,10 @@ import { useTranslation } from "react-i18next";
import { AttributeForm } from "../components/key-value-form/AttributeForm";
import { MultiLineInput } from "../components/multi-line-input/MultiLineInput";
import { keyValueToArray } from "../components/key-value-form/key-value-convert";
import { useParams } from "react-router-dom";
import { EditOrganizationParams } from "./routes/EditOrganization";
import { useFormContext, useWatch } from "react-hook-form";
import { useEffect } from "react";
export type OrganizationFormType = AttributeForm &
Omit<OrganizationRepresentation, "domains" | "attributes"> & {
@ -25,6 +29,19 @@ export const convertToOrg = (
export const OrganizationForm = () => {
const { t } = useTranslation();
const { tab } = useParams<EditOrganizationParams>();
const { setValue, getFieldState } = useFormContext();
const name = useWatch({ name: "name" });
const isEditable = tab !== "settings";
useEffect(() => {
const { isDirty } = getFieldState("alias");
if (isEditable && !isDirty) {
setValue("alias", name);
}
}, [name, isEditable]);
return (
<>
<TextControl
@ -32,6 +49,12 @@ export const OrganizationForm = () => {
name="name"
rules={{ required: t("required") }}
/>
<TextControl
label={t("alias")}
name="alias"
labelIcon={t("organizationAliasHelp")}
isDisabled={!isEditable}
/>
<FormGroup
label={t("domain")}
fieldId="domain"

View file

@ -34,6 +34,7 @@ public class CachedOrganization extends AbstractRevisioned implements InRealm {
private final RealmModel realm;
private final String name;
private final String alias;
private final String description;
private final boolean enabled;
private final LazyLoader<OrganizationModel, MultivaluedHashMap<String, String>> attributes;
@ -44,6 +45,7 @@ public class CachedOrganization extends AbstractRevisioned implements InRealm {
super(revision, organization.getId());
this.realm = realm;
this.name = organization.getName();
this.alias = organization.getAlias();
this.description = organization.getDescription();
this.enabled = organization.isEnabled();
this.attributes = new DefaultLazyLoader<>(orgModel -> new MultivaluedHashMap<>(orgModel.getAttributes()), MultivaluedHashMap::new);
@ -64,6 +66,10 @@ public class CachedOrganization extends AbstractRevisioned implements InRealm {
return name;
}
public String getAlias() {
return alias;
}
public String getDescription() {
return description;
}

View file

@ -48,9 +48,9 @@ public class InfinispanOrganizationProvider implements OrganizationProvider {
}
@Override
public OrganizationModel create(String name) {
public OrganizationModel create(String name, String alias) {
registerCountInvalidation();
return orgDelegate.create(name);
return orgDelegate.create(name, alias);
}
@Override

View file

@ -86,6 +86,18 @@ public class OrganizationAdapter implements OrganizationModel {
updated.setName(name);
}
@Override
public String getAlias() {
if (isUpdated()) return updated.getAlias() ;
return cached.getAlias();
}
@Override
public void setAlias(String alias) {
getDelegateForUpdate();
updated.setAlias(alias);
}
@Override
public boolean isEnabled() {
if (isUpdated()) return updated.isEnabled();
@ -145,4 +157,17 @@ public class OrganizationAdapter implements OrganizationModel {
return delegate.isManagedMember(this, user);
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof OrganizationModel)) return false;
OrganizationModel that = (OrganizationModel) o;
return that.getId().equals(getId());
}
@Override
public int hashCode() {
return getId().hashCode();
}
}

View file

@ -55,6 +55,9 @@ public class OrganizationEntity {
@Column(name = "NAME")
private String name;
@Column(name = "ALIAS")
private String alias;
@Column(name = "ENABLED")
private boolean enabled;
@ -82,6 +85,14 @@ public class OrganizationEntity {
this.name = name;
}
public String getAlias() {
return alias;
}
public void setAlias(String alias) {
this.alias = alias;
}
public boolean isEnabled() {
return this.enabled;
}

View file

@ -70,15 +70,23 @@ public class JpaOrganizationProvider implements OrganizationProvider {
}
@Override
public OrganizationModel create(String name) {
public OrganizationModel create(String name, String alias) {
if (StringUtil.isBlank(name)) {
throw new ModelValidationException("Name can not be null");
}
if (StringUtil.isBlank(alias)) {
alias = name;
}
if (getByName(name) != null) {
throw new ModelDuplicateException("A organization with the same name already exists.");
}
if (getAllStream(Map.of(OrganizationModel.ALIAS, alias), -1, -1).findAny().isPresent()) {
throw new ModelDuplicateException("A organization with the same alias already exists");
}
RealmModel realm = getRealm();
OrganizationAdapter adapter = new OrganizationAdapter(realm, this);
@ -88,6 +96,7 @@ public class JpaOrganizationProvider implements OrganizationProvider {
adapter.setGroupId(group.getId());
adapter.setName(name);
adapter.setAlias(alias);
adapter.setEnabled(true);
em.persist(adapter.getEntity());
@ -224,7 +233,13 @@ public class JpaOrganizationProvider implements OrganizationProvider {
predicates.add(builder.equal(org.get("groupId"), group.get("id")));
for (Map.Entry<String, String> entry : attributes.entrySet()) {
if (StringUtil.isNotBlank(entry.getKey())) {
if (StringUtil.isBlank(entry.getKey())) {
continue;
}
if (OrganizationModel.ALIAS.equals(entry.getKey())) {
predicates.add(builder.equal(org.get("alias"), entry.getValue()));
} else {
Join<GroupEntity, GroupAttributeEntity> groupJoin = group.join("attributes");
Predicate attrNamePredicate = builder.equal(groupJoin.get("name"), entry.getKey());
Predicate attrValuePredicate = builder.equal(groupJoin.get("value"), entry.getValue());

View file

@ -93,6 +93,25 @@ public final class OrganizationAdapter implements OrganizationModel, JpaModel<Or
return entity.getName();
}
@Override
public String getAlias() {
return entity.getAlias();
}
@Override
public void setAlias(String alias) {
if (StringUtil.isBlank(alias)) {
alias = getName();
}
if (alias.equals(entity.getAlias())) {
return;
}
if (StringUtil.isNotBlank(entity.getAlias())) {
throw new ModelValidationException("Cannot change the alias");
}
entity.setAlias(alias);
}
@Override
public boolean isEnabled() {
return provider.isEnabled() && entity.isEnabled();

View file

@ -0,0 +1,31 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!--
~ * Copyright 2024 Red Hat, Inc. and/or its affiliates
~ * and other contributors as indicated by the @author tags.
~ *
~ * 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.
-->
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.1.xsd">
<changeSet author="keycloak" id="26.0.0-org-alias">
<addColumn tableName="ORG">
<column name="ALIAS" type="VARCHAR(255)"/>
</addColumn>
<update tableName="ORG">
<column name="ALIAS" valueComputed="NAME"/>
</update>
<addNotNullConstraint tableName="ORG" columnName="ALIAS" columnDataType="VARCHAR(255)"/>
<addUniqueConstraint tableName="ORG" columnNames="REALM_ID, ALIAS" constraintName="UK_ORG_ALIAS"/>
</changeSet>
</databaseChangeLog>

View file

@ -82,5 +82,6 @@
<include file="META-INF/jpa-changelog-24.0.0.xml"/>
<include file="META-INF/jpa-changelog-24.0.2.xml"/>
<include file="META-INF/jpa-changelog-25.0.0.xml"/>
<include file="META-INF/jpa-changelog-26.0.0.xml"/>
</databaseChangeLog>

View file

@ -265,6 +265,7 @@ public class ExportUtils {
OrganizationRepresentation org = new OrganizationRepresentation();
org.setName(m.getName());
org.setAlias(m.getAlias());
org.setEnabled(m.isEnabled());
org.setDescription(m.getDescription());
m.getDomains().map(d -> {

View file

@ -1589,7 +1589,8 @@ public class DefaultExportImportManager implements ExportImportManager {
OrganizationProvider provider = session.getProvider(OrganizationProvider.class);
for (OrganizationRepresentation orgRep : Optional.ofNullable(rep.getOrganizations()).orElse(Collections.emptyList())) {
OrganizationModel org = provider.create(orgRep.getName());
OrganizationModel org = provider.create(orgRep.getName(), orgRep.getAlias());
org.setDomains(orgRep.getDomains().stream().map(r -> new OrganizationDomainModel(r.getName(), r.isVerified())).collect(Collectors.toSet()));
for (IdentityProviderRepresentation identityProvider : Optional.ofNullable(orgRep.getIdentityProviders()).orElse(Collections.emptyList())) {

View file

@ -28,6 +28,7 @@ public interface OrganizationModel {
String ORGANIZATION_NAME_ATTRIBUTE = "kc.org.name";
String ORGANIZATION_DOMAIN_ATTRIBUTE = "kc.org.domain";
String BROKER_PUBLIC = "kc.org.broker.public";
String ALIAS = "alias";
enum IdentityProviderRedirectMode {
EMAIL_MATCH("kc.org.broker.redirect.mode.email-matches");
@ -53,6 +54,10 @@ public interface OrganizationModel {
String getName();
String getAlias();
void setAlias(String alias);
boolean isEnabled();
void setEnabled(boolean enabled);

View file

@ -34,10 +34,11 @@ public interface OrganizationProvider extends Provider {
* Creates a new organization with given {@code name} to the realm.
* The internal ID of the organization will be created automatically.
* @param name String name of the organization.
* @throws ModelDuplicateException If there is already an organization with the given name
* @param alias the alias of the organization. If not set, defaults to the value set to {@code name}. Once set, the alias is immutable.
* @throws ModelDuplicateException If there is already an organization with the given name or alias
* @return Model of the created organization.
*/
OrganizationModel create(String name);
OrganizationModel create(String name, String alias);
/**
* Returns a {@link OrganizationModel} by its {@code id};

View file

@ -97,6 +97,8 @@ public class UpdateProfile implements RequiredActionProvider, RequiredActionFact
form = form.setFormData(formData);
}
form.setUser(context.getUser());
return form.createResponse(getResponseAction());
}

View file

@ -47,6 +47,7 @@ import org.keycloak.forms.login.freemarker.model.IdpReviewProfileBean;
import org.keycloak.forms.login.freemarker.model.LoginBean;
import org.keycloak.forms.login.freemarker.model.LogoutConfirmBean;
import org.keycloak.forms.login.freemarker.model.OAuthGrantBean;
import org.keycloak.forms.login.freemarker.model.OrganizationBean;
import org.keycloak.forms.login.freemarker.model.ProfileBean;
import org.keycloak.forms.login.freemarker.model.RealmBean;
import org.keycloak.forms.login.freemarker.model.RecoveryAuthnCodeInputLoginBean;
@ -63,6 +64,7 @@ import org.keycloak.models.ClientModel;
import org.keycloak.models.Constants;
import org.keycloak.models.IdentityProviderModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.OrganizationModel;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.utils.FormMessage;
@ -101,6 +103,7 @@ import java.util.Properties;
import java.util.function.Function;
import static org.keycloak.models.UserModel.RequiredAction.UPDATE_PASSWORD;
import static org.keycloak.organization.utils.Organizations.resolveOrganization;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
@ -540,6 +543,14 @@ public class FreeMarkerLoginFormsProvider implements LoginFormsProvider {
attributes.put("locale", new LocaleBean(realm, locale, b, messagesBundle));
}
if (Profile.isFeatureEnabled(Feature.ORGANIZATION)) {
OrganizationModel organization = resolveOrganization(session, user);
if (organization != null) {
attributes.put("org", new OrganizationBean(session, organization, user));
}
}
}
if (realm != null && user != null && session != null) {
attributes.put("authenticatorConfigured", new AuthenticatorConfiguredMethod(realm, user, session));

View file

@ -0,0 +1,71 @@
/*
* Copyright 2024 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* 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.
*/
package org.keycloak.forms.login.freemarker.model;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.OrganizationDomainModel;
import org.keycloak.models.OrganizationModel;
import org.keycloak.models.UserModel;
import org.keycloak.organization.OrganizationProvider;
public class OrganizationBean {
private final String name;
private final String alias;
private final Set<String> domains;
private final boolean isMember;
private final Map<String, List<String>> attributes;
public OrganizationBean(KeycloakSession session, OrganizationModel organization, UserModel user) {
this.name = organization.getName();
this.alias = organization.getAlias();
this.domains = organization.getDomains().map(OrganizationDomainModel::getName).collect(Collectors.toSet());
this.isMember = user != null && organization.equals(getOrganizationProvider(session).getByMember(user));
this.attributes = Collections.unmodifiableMap(organization.getAttributes());
}
private static OrganizationProvider getOrganizationProvider(KeycloakSession session) {
return session.getProvider(OrganizationProvider.class);
}
public String getName() {
return name;
}
public String getAlias() {
return alias;
}
public Set<String> getDomains() {
return domains;
}
public Map<String, List<String>> getAttributes() {
return attributes;
}
public boolean isMember() {
return isMember;
}
}

View file

@ -31,6 +31,7 @@ import jakarta.ws.rs.Produces;
import jakarta.ws.rs.QueryParam;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.core.Response.Status;
import jakarta.ws.rs.ext.Provider;
import org.eclipse.microprofile.openapi.annotations.Operation;
import org.eclipse.microprofile.openapi.annotations.extensions.Extension;
@ -38,6 +39,7 @@ import org.eclipse.microprofile.openapi.annotations.parameters.Parameter;
import org.eclipse.microprofile.openapi.annotations.tags.Tag;
import org.jboss.resteasy.reactive.NoCache;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.ModelDuplicateException;
import org.keycloak.models.ModelValidationException;
import org.keycloak.models.OrganizationModel;
import org.keycloak.organization.OrganizationProvider;
@ -90,13 +92,15 @@ public class OrganizationsResource {
}
try {
OrganizationModel model = provider.create(organization.getName());
OrganizationModel model = provider.create(organization.getName(), organization.getAlias());
Organizations.toModel(organization, model);
return Response.created(session.getContext().getUri().getAbsolutePathBuilder().path(model.getId()).build()).build();
} catch (ModelValidationException mve) {
throw ErrorResponse.error(mve.getMessage(), Response.Status.BAD_REQUEST);
} catch (ModelDuplicateException mve) {
throw ErrorResponse.error(mve.getMessage(), Status.CONFLICT);
}
}

View file

@ -17,6 +17,7 @@
package org.keycloak.organization.authentication.authenticators.browser;
import static org.keycloak.organization.utils.Organizations.getEmailDomain;
import static org.keycloak.organization.utils.Organizations.isEnabledAndOrganizationsPresent;
import static org.keycloak.organization.utils.Organizations.resolveBroker;
@ -29,6 +30,7 @@ import org.keycloak.authentication.authenticators.browser.IdentityProviderAuthen
import org.keycloak.forms.login.LoginFormsProvider;
import org.keycloak.forms.login.freemarker.model.AuthenticationContextBean;
import org.keycloak.forms.login.freemarker.model.IdentityProviderBean;
import org.keycloak.forms.login.freemarker.model.OrganizationBean;
import org.keycloak.http.HttpRequest;
import org.keycloak.models.IdentityProviderModel;
import org.keycloak.models.KeycloakSession;
@ -77,6 +79,14 @@ public class OrganizationAuthenticator extends IdentityProviderAuthenticator {
return;
}
OrganizationProvider provider = getOrganizationProvider();
OrganizationModel organization = provider.getByDomainName(emailDomain);
if (organization != null) {
// make sure the organization is set to the session to make it available to templates
session.setAttribute(OrganizationModel.class.getName(), organization);
}
RealmModel realm = context.getRealm();
UserModel user = session.users().getUserByEmail(realm, username);
@ -91,6 +101,12 @@ public class OrganizationAuthenticator extends IdentityProviderAuthenticator {
if (broker.isEmpty()) {
// not a managed member, continue with the regular flow
if (organization != null) {
context.form().setAttributeMapper(attributes -> {
attributes.put("org", new OrganizationBean(session, organization, user));
return attributes;
});
}
context.attempted();
} else if (broker.size() == 1) {
// user is a managed member and associated with a broker, redirect automatically
@ -100,9 +116,6 @@ public class OrganizationAuthenticator extends IdentityProviderAuthenticator {
return;
}
OrganizationProvider provider = getOrganizationProvider();
OrganizationModel organization = provider.getByDomainName(emailDomain);
if (organization == null || !organization.isEnabled()) {
// request does not map to any organization, go to the next step/sub-flow
context.attempted();
@ -173,20 +186,6 @@ public class OrganizationAuthenticator extends IdentityProviderAuthenticator {
context.challenge(form.createLoginUsername());
}
private String getEmailDomain(String email) {
if (email == null) {
return null;
}
int domainSeparator = email.indexOf('@');
if (domainSeparator == -1) {
return null;
}
return email.substring(domainSeparator + 1);
}
@Override
public boolean configuredFor(KeycloakSession session, RealmModel realm, UserModel user) {
return realm.isOrganizationsEnabled();

View file

@ -91,7 +91,7 @@ public class OrganizationMembershipMapper extends AbstractOIDCProtocolMapper imp
}
Map<String, Map<String, Object>> claim = new HashMap<>();
claim.put(organization.getName(), Map.of());
claim.put(organization.getAlias(), Map.of());
token.getOtherClaims().put(OAuth2Constants.ORGANIZATION, claim);
}

View file

@ -74,7 +74,7 @@ public class OrganizationMembershipMapper extends AbstractSAMLProtocolMapper imp
AttributeType attribute = new AttributeType(ORGANIZATION_ATTRIBUTE_NAME);
attribute.setFriendlyName(ORGANIZATION_ATTRIBUTE_NAME);
attribute.setNameFormat(JBossSAMLURIConstants.ATTRIBUTE_FORMAT_BASIC.get());
attribute.addAttributeValue(organization.getName());
attribute.addAttributeValue(organization.getAlias());
attributeStatement.addAttribute(new AttributeStatementType.ASTChoiceType(attribute));
}

View file

@ -146,6 +146,7 @@ public class Organizations {
rep.setId(model.getId());
rep.setName(model.getName());
rep.setAlias(model.getAlias());
rep.setEnabled(model.isEnabled());
rep.setDescription(model.getDescription());
rep.setAttributes(model.getAttributes());
@ -168,6 +169,7 @@ public class Organizations {
}
model.setName(rep.getName());
model.setAlias(rep.getAlias());
model.setEnabled(rep.isEnabled());
model.setDescription(rep.getDescription());
model.setAttributes(rep.getAttributes());
@ -194,4 +196,41 @@ public class Organizations {
return TokenVerifier.create(tokenFromQuery, InviteOrgActionToken.class).getToken();
}
public static String getEmailDomain(String email) {
if (email == null) {
return null;
}
int domainSeparator = email.indexOf('@');
if (domainSeparator == -1) {
return null;
}
return email.substring(domainSeparator + 1);
}
public static OrganizationModel resolveOrganization(KeycloakSession session, UserModel user) {
OrganizationModel organization = (OrganizationModel) session.getAttribute(OrganizationModel.class.getName());
if (organization != null) {
return organization;
}
if (user == null) {
return null;
}
OrganizationProvider provider = session.getProvider(OrganizationProvider.class);
OrganizationModel memberOrg = provider.getByMember(user);
if (memberOrg != null) {
return memberOrg;
}
String domain = Organizations.getEmailDomain(user.getEmail());
return domain == null ? null : provider.getByDomainName(domain);
}
}

View file

@ -5,5 +5,9 @@
}, {
"name" : "incorrect",
"types": [ "admin" ]
},
{
"name" : "organization",
"types": [ "login" ]
}]
}

View file

@ -0,0 +1,25 @@
<#import "template.ftl" as layout>
<#import "user-profile-commons.ftl" as userProfileCommons>
<#import "test-org-commons.ftl" as commons>
<@layout.registrationLayout displayMessage=messagesPerField.exists('global') displayRequiredFields=true; section>
<#if section = "header">
${msg("loginIdpReviewProfileTitle")}
<@commons.assertions org/>
<#elseif section = "form">
<form id="kc-idp-review-profile-form" class="${properties.kcFormClass!}" action="${url.loginAction}" method="post">
<@userProfileCommons.userProfileFormFields/>
<div class="${properties.kcFormGroupClass!}">
<div id="kc-form-options" class="${properties.kcFormOptionsClass!}">
<div class="${properties.kcFormOptionsWrapperClass!}">
</div>
</div>
<div id="kc-form-buttons" class="${properties.kcFormButtonsClass!}">
<input class="${properties.kcButtonClass!} ${properties.kcButtonPrimaryClass!} ${properties.kcButtonBlockClass!} ${properties.kcButtonLargeClass!}" type="submit" value="${msg("doSubmit")}" />
</div>
</div>
</form>
</#if>
</@layout.registrationLayout>

View file

@ -0,0 +1,30 @@
<#import "template.ftl" as layout>
<#import "user-profile-commons.ftl" as userProfileCommons>
<#import "test-org-commons.ftl" as commons>
<@layout.registrationLayout displayMessage=messagesPerField.exists('global') displayRequiredFields=true; section>
<#if section = "header">
${msg("loginProfileTitle")}
<@commons.assertions org/>
<#elseif section = "form">
<form id="kc-update-profile-form" class="${properties.kcFormClass!}" action="${url.loginAction}" method="post">
<@userProfileCommons.userProfileFormFields/>
<div class="${properties.kcFormGroupClass!}">
<div id="kc-form-options" class="${properties.kcFormOptionsClass!}">
<div class="${properties.kcFormOptionsWrapperClass!}">
</div>
</div>
<div id="kc-form-buttons" class="${properties.kcFormButtonsClass!}">
<#if isAppInitiatedAction??>
<input class="${properties.kcButtonClass!} ${properties.kcButtonPrimaryClass!} ${properties.kcButtonLargeClass!}" type="submit" value="${msg("doSubmit")}" />
<button class="${properties.kcButtonClass!} ${properties.kcButtonDefaultClass!} ${properties.kcButtonLargeClass!}" type="submit" name="cancel-aia" value="true" formnovalidate/>${msg("doCancel")}</button>
<#else>
<input class="${properties.kcButtonClass!} ${properties.kcButtonPrimaryClass!} ${properties.kcButtonBlockClass!} ${properties.kcButtonLargeClass!}" type="submit" value="${msg("doSubmit")}" />
</#if>
</div>
</div>
</form>
</#if>
</@layout.registrationLayout>

View file

@ -0,0 +1,88 @@
<#import "template.ftl" as layout>
<#import "test-org-commons.ftl" as commons>
<@layout.registrationLayout displayMessage=!messagesPerField.existsError('username') displayInfo=(realm.password && realm.registrationAllowed && !registrationDisabled??); section>
<#if section = "header">
<@commons.assertions org/>
<#elseif section = "form">
<div id="kc-form">
<div id="kc-form-wrapper">
<#if realm.password>
<form id="kc-form-login" onsubmit="login.disabled = true; return true;" action="${url.loginAction}"
method="post">
<#if !usernameHidden??>
<div class="${properties.kcFormGroupClass!}">
<label for="username"
class="${properties.kcLabelClass!}"><#if !realm.loginWithEmailAllowed>${msg("username")}<#elseif !realm.registrationEmailAsUsername>${msg("usernameOrEmail")}<#else>${msg("email")}</#if></label>
<input tabindex="1" id="username"
aria-invalid="<#if messagesPerField.existsError('username')>true</#if>"
class="${properties.kcInputClass!}" name="username"
value="${(login.username!'')}"
type="text" autofocus autocomplete="off"/>
<#if messagesPerField.existsError('username')>
<span id="input-error-username" class="${properties.kcInputErrorMessageClass!}" aria-live="polite">
${kcSanitize(messagesPerField.get('username'))?no_esc}
</span>
</#if>
</div>
</#if>
<div class="${properties.kcFormGroupClass!} ${properties.kcFormSettingClass!}">
<div id="kc-form-options">
<#if realm.rememberMe && !usernameHidden??>
<div class="checkbox">
<label>
<#if login.rememberMe??>
<input tabindex="3" id="rememberMe" name="rememberMe" type="checkbox"
checked> ${msg("rememberMe")}
<#else>
<input tabindex="3" id="rememberMe" name="rememberMe"
type="checkbox"> ${msg("rememberMe")}
</#if>
</label>
</div>
</#if>
</div>
</div>
<div id="kc-form-buttons" class="${properties.kcFormGroupClass!}">
<input tabindex="4"
class="${properties.kcButtonClass!} ${properties.kcButtonPrimaryClass!} ${properties.kcButtonBlockClass!} ${properties.kcButtonLargeClass!}"
name="login" id="kc-login" type="submit" value="${msg("doLogIn")}"/>
</div>
</form>
</#if>
</div>
</div>
<#elseif section = "info" >
<#if realm.password && realm.registrationAllowed && !registrationDisabled??>
<div id="kc-registration">
<span>${msg("noAccount")} <a tabindex="6" href="${url.registrationUrl}">${msg("doRegister")}</a></span>
</div>
</#if>
<#elseif section = "socialProviders" >
<#if realm.password && social?? && social.providers?has_content>
<div id="kc-social-providers" class="${properties.kcFormSocialAccountSectionClass!}">
<hr/>
<h4>${msg("identity-provider-login-label")}</h4>
<ul class="${properties.kcFormSocialAccountListClass!} <#if social.providers?size gt 3>${properties.kcFormSocialAccountListGridClass!}</#if>">
<#list social.providers as p>
<a id="social-${p.alias}" class="${properties.kcFormSocialAccountListButtonClass!} <#if social.providers?size gt 3>${properties.kcFormSocialAccountGridItem!}</#if>"
type="button" href="${p.loginUrl}">
<#if p.iconClasses?has_content>
<i class="${properties.kcCommonLogoIdP!} ${p.iconClasses!}" aria-hidden="true"></i>
<span class="${properties.kcFormSocialAccountNameClass!} kc-social-icon-text">${p.displayName!}</span>
<#else>
<span class="${properties.kcFormSocialAccountNameClass!}">${p.displayName!}</span>
</#if>
</a>
</#list>
</ul>
</div>
</#if>
</#if>
</@layout.registrationLayout>

View file

@ -0,0 +1,116 @@
<#import "template.ftl" as layout>
<#import "test-org-commons.ftl" as commons>
<@layout.registrationLayout displayMessage=!messagesPerField.existsError('username','password') displayInfo=realm.password && realm.registrationAllowed && !registrationDisabled??; section>
<#if section = "header">
<@commons.assertions org/>
<#elseif section = "form">
<div id="kc-form">
<div id="kc-form-wrapper">
<#if realm.password>
<form id="kc-form-login" onsubmit="login.disabled = true; return true;" action="${url.loginAction}" method="post">
<#if !usernameHidden??>
<div class="${properties.kcFormGroupClass!}">
<label for="username" class="${properties.kcLabelClass!}"><#if !realm.loginWithEmailAllowed>${msg("username")}<#elseif !realm.registrationEmailAsUsername>${msg("usernameOrEmail")}<#else>${msg("email")}</#if></label>
<input tabindex="2" id="username" class="${properties.kcInputClass!}" name="username" value="${(login.username!'')}" type="text" autofocus autocomplete="username"
aria-invalid="<#if messagesPerField.existsError('username','password')>true</#if>"
/>
<#if messagesPerField.existsError('username','password')>
<span id="input-error" class="${properties.kcInputErrorMessageClass!}" aria-live="polite">
${kcSanitize(messagesPerField.getFirstError('username','password'))?no_esc}
</span>
</#if>
</div>
</#if>
<div class="${properties.kcFormGroupClass!}">
<label for="password" class="${properties.kcLabelClass!}">${msg("password")}</label>
<div class="${properties.kcInputGroup!}">
<input tabindex="3" id="password" class="${properties.kcInputClass!}" name="password" type="password" autocomplete="current-password"
aria-invalid="<#if messagesPerField.existsError('username','password')>true</#if>"
/>
<button class="${properties.kcFormPasswordVisibilityButtonClass!}" type="button" aria-label="${msg("showPassword")}"
aria-controls="password" data-password-toggle tabindex="4"
data-icon-show="${properties.kcFormPasswordVisibilityIconShow!}" data-icon-hide="${properties.kcFormPasswordVisibilityIconHide!}"
data-label-show="${msg('showPassword')}" data-label-hide="${msg('hidePassword')}">
<i class="${properties.kcFormPasswordVisibilityIconShow!}" aria-hidden="true"></i>
</button>
</div>
<#if usernameHidden?? && messagesPerField.existsError('username','password')>
<span id="input-error" class="${properties.kcInputErrorMessageClass!}" aria-live="polite">
${kcSanitize(messagesPerField.getFirstError('username','password'))?no_esc}
</span>
</#if>
</div>
<div class="${properties.kcFormGroupClass!} ${properties.kcFormSettingClass!}">
<div id="kc-form-options">
<#if realm.rememberMe && !usernameHidden??>
<div class="checkbox">
<label>
<#if login.rememberMe??>
<input tabindex="5" id="rememberMe" name="rememberMe" type="checkbox" checked> ${msg("rememberMe")}
<#else>
<input tabindex="5" id="rememberMe" name="rememberMe" type="checkbox"> ${msg("rememberMe")}
</#if>
</label>
</div>
</#if>
</div>
<div class="${properties.kcFormOptionsWrapperClass!}">
<#if realm.resetPasswordAllowed>
<span><a tabindex="6" href="${url.loginResetCredentialsUrl}">${msg("doForgotPassword")}</a></span>
</#if>
</div>
</div>
<div id="kc-form-buttons" class="${properties.kcFormGroupClass!}">
<input type="hidden" id="id-hidden-input" name="credentialId" <#if auth.selectedCredential?has_content>value="${auth.selectedCredential}"</#if>/>
<input tabindex="7" class="${properties.kcButtonClass!} ${properties.kcButtonPrimaryClass!} ${properties.kcButtonBlockClass!} ${properties.kcButtonLargeClass!}" name="login" id="kc-login" type="submit" value="${msg("doLogIn")}"/>
</div>
</form>
</#if>
</div>
</div>
<script type="module" src="${url.resourcesPath}/js/passwordVisibility.js"></script>
<#elseif section = "info" >
<#if realm.password && realm.registrationAllowed && !registrationDisabled??>
<div id="kc-registration-container">
<div id="kc-registration">
<span>${msg("noAccount")} <a tabindex="8"
href="${url.registrationUrl}">${msg("doRegister")}</a></span>
</div>
</div>
</#if>
<#elseif section = "socialProviders" >
<#if realm.password && social?? && social.providers?has_content>
<div id="kc-social-providers" class="${properties.kcFormSocialAccountSectionClass!}">
<hr/>
<h2>${msg("identity-provider-login-label")}</h2>
<ul class="${properties.kcFormSocialAccountListClass!} <#if social.providers?size gt 3>${properties.kcFormSocialAccountListGridClass!}</#if>">
<#list social.providers as p>
<li>
<a id="social-${p.alias}" class="${properties.kcFormSocialAccountListButtonClass!} <#if social.providers?size gt 3>${properties.kcFormSocialAccountGridItem!}</#if>"
type="button" href="${p.loginUrl}">
<#if p.iconClasses?has_content>
<i class="${properties.kcCommonLogoIdP!} ${p.iconClasses!}" aria-hidden="true"></i>
<span class="${properties.kcFormSocialAccountNameClass!} kc-social-icon-text">${p.displayName!}</span>
<#else>
<span class="${properties.kcFormSocialAccountNameClass!}">${p.displayName!}</span>
</#if>
</a>
</li>
</#list>
</ul>
</div>
</#if>
</#if>
</@layout.registrationLayout>

View file

@ -0,0 +1,13 @@
<#macro assertions org="">
<#if org?has_content>
Sign-in to ${org.name} organization
<#list org.attributes?keys as key>
The ${key} is ${org.attributes[key]}
</#list>
<#if org.member>
User is member of ${org.name}
</#if>
<#else>
Sign-in to the realm
</#if>
</#macro>

View file

@ -0,0 +1,18 @@
#
# Copyright 2024 Red Hat, Inc. and/or its affiliates
# and other contributors as indicated by the @author tags.
#
# 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.
#
parent=keycloak

View file

@ -59,7 +59,7 @@ public class ServerInfoTest extends AbstractKeycloakTest {
Assert.assertNames(info.getThemes().get("account"), "base", "keycloak.v3", "custom-account-provider");
Assert.assertNames(info.getThemes().get("admin"), "base", "keycloak.v2");
Assert.assertNames(info.getThemes().get("email"), "base", "keycloak");
Assert.assertNames(info.getThemes().get("login"), "address", "base", "environment-agnostic", "keycloak");
Assert.assertNames(info.getThemes().get("login"), "address", "base", "environment-agnostic", "keycloak", "organization");
Assert.assertNames(info.getThemes().get("welcome"), "keycloak");
assertNotNull(info.getEnums());

View file

@ -133,6 +133,7 @@ public abstract class AbstractOrganizationTest extends AbstractAdminTest {
protected OrganizationRepresentation createRepresentation(String name, String... orgDomains) {
OrganizationRepresentation org = new OrganizationRepresentation();
org.setName(name);
org.setAlias(name);
for (String orgDomain : orgDomains) {
OrganizationDomainRepresentation domainRep = new OrganizationDomainRepresentation();

View file

@ -45,11 +45,13 @@ import java.io.IOException;
import java.util.stream.IntStream;
import org.junit.Test;
import org.keycloak.admin.client.resource.OrganizationResource;
import org.keycloak.admin.client.resource.OrganizationsResource;
import org.keycloak.admin.client.resource.RealmResource;
import org.keycloak.common.Profile.Feature;
import org.keycloak.models.OrganizationModel;
import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.organization.OrganizationProvider;
import org.keycloak.representations.idm.ErrorRepresentation;
import org.keycloak.representations.idm.IdentityProviderRepresentation;
import org.keycloak.representations.idm.OrganizationDomainRepresentation;
import org.keycloak.representations.idm.OrganizationRepresentation;
@ -80,6 +82,7 @@ public class OrganizationTest extends AbstractOrganizationTest {
OrganizationRepresentation existing = organization.toRepresentation();
assertEquals(expected.getId(), existing.getId());
assertEquals(expected.getName(), existing.getName());
assertEquals(expected.getAlias(), existing.getAlias());
assertEquals(1, existing.getDomains().size());
assertThat(existing.isEnabled(), is(false));
assertThat(existing.getDescription(), notNullValue());
@ -463,4 +466,43 @@ public class OrganizationTest extends AbstractOrganizationTest {
assertEquals(9, orgProvider.count());
});
}
@Test
public void testFailUpdateAlias() {
OrganizationRepresentation rep = createOrganization();
rep.setAlias("changed");
OrganizationsResource organizations = testRealm().organizations();
OrganizationResource organization = organizations.get(rep.getId());
try (Response response = organization.update(rep)) {
assertEquals(Status.BAD_REQUEST.getStatusCode(), response.getStatus());
ErrorRepresentation error = response.readEntity(ErrorRepresentation.class);
assertEquals("Cannot change the alias", error.getErrorMessage());
}
rep.setAlias(rep.getName());
try (Response response = organization.update(rep)) {
assertEquals(Status.NO_CONTENT.getStatusCode(), response.getStatus());
}
}
@Test
public void testFailDuplicatedAlias() {
OrganizationRepresentation rep = createOrganization();
OrganizationsResource organizations = testRealm().organizations();
rep.setId(null);
rep.getDomains().clear();
rep.addDomain(new OrganizationDomainRepresentation("acme-2"));
rep.setName("acme-2");
try (Response response = organizations.create(rep)) {
assertEquals(Status.CONFLICT.getStatusCode(), response.getStatus());
ErrorRepresentation error = response.readEntity(ErrorRepresentation.class);
assertEquals("A organization with the same alias already exists", error.getErrorMessage());
}
}
}

View file

@ -0,0 +1,212 @@
/*
* Copyright 2024 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* 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.
*
*/
package org.keycloak.testsuite.organization.admin;
import static org.keycloak.testsuite.broker.BrokerTestTools.waitForPage;
import java.util.List;
import java.util.Map.Entry;
import jakarta.ws.rs.core.Response;
import org.jboss.arquillian.graphene.page.Page;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.keycloak.admin.client.resource.OrganizationResource;
import org.keycloak.admin.client.resource.RealmResource;
import org.keycloak.common.Profile.Feature;
import org.keycloak.models.OrganizationModel.IdentityProviderRedirectMode;
import org.keycloak.models.UserModel;
import org.keycloak.representations.idm.IdentityProviderRepresentation;
import org.keycloak.representations.idm.OrganizationRepresentation;
import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.representations.idm.UserRepresentation;
import org.keycloak.testsuite.admin.ApiUtil;
import org.keycloak.testsuite.arquillian.annotation.EnableFeature;
import org.keycloak.testsuite.pages.AppPage;
import org.keycloak.testsuite.pages.LoginPage;
import org.keycloak.testsuite.pages.LoginUpdateProfilePage;
import org.keycloak.testsuite.util.UserBuilder;
@EnableFeature(Feature.ORGANIZATION)
public class OrganizationThemeTest extends AbstractOrganizationTest {
@Page
protected LoginPage loginPage;
@Page
protected LoginUpdateProfilePage updateProfilePage;
@Page
protected AppPage appPage;
@Before
public void onBefore() {
RealmResource realm = realmsResouce().realm(bc.consumerRealmName());
RealmRepresentation rep = realm.toRepresentation();
rep.setLoginTheme("organization");
realm.update(rep);
}
@Test
public void testOrganizationOnRegularLogin() {
OrganizationResource organization = testRealm().organizations().get(createOrganization("myorg", "myorg.com").getId());
IdentityProviderRepresentation broker = organization.identityProviders().getIdentityProviders().get(0);
broker.getConfig().remove(IdentityProviderRedirectMode.EMAIL_MATCH.getKey());
testRealm().identityProviders().get(broker.getAlias()).update(broker);
UserRepresentation user = UserBuilder.create().enabled(true)
.username("tom")
.email("tom@myorg.com")
.password("password")
.firstName("Tom")
.lastName("Brady")
.build();
try (Response resp = realmsResouce().realm(bc.consumerRealmName()).users().create(user)) {
String userId = ApiUtil.getCreatedId(resp);
getCleanup(bc.consumerRealmName()).addUserId(userId);
}
// organization available to regular login page
loginPage.open(bc.consumerRealmName());
Assert.assertTrue(driver.getPageSource().contains("Sign-in to the realm"));
loginPage.loginUsername("tom@myorg.com");
Assert.assertTrue(driver.getPageSource().contains("Sign-in to myorg organization"));
Assert.assertTrue(loginPage.isPasswordInputPresent());
}
@Test
public void testOrganizationOnIdentityFirstLogin() {
OrganizationResource organization = testRealm().organizations().get(createOrganization("myorg", "myorg.com").getId());
IdentityProviderRepresentation broker = organization.identityProviders().getIdentityProviders().get(0);
broker.getConfig().remove(IdentityProviderRedirectMode.EMAIL_MATCH.getKey());
testRealm().identityProviders().get(broker.getAlias()).update(broker);
// organization available to identity-first login page
loginPage.open(bc.consumerRealmName());
Assert.assertTrue(driver.getPageSource().contains("Sign-in to the realm"));
Assert.assertFalse(loginPage.isPasswordInputPresent());
loginPage.loginUsername("non-user@myorg.com");
Assert.assertTrue(driver.getPageSource().contains("Sign-in to myorg organization"));
Assert.assertFalse(loginPage.isPasswordInputPresent());
}
@Test
public void testOrganizationOnIdPReview() {
UserRepresentation user = UserBuilder.create().enabled(true)
.username("tom")
.password("password")
.firstName("Tom")
.lastName("Brady")
.build();
try (Response resp = realmsResouce().realm(bc.providerRealmName()).users().create(user)) {
String userId = ApiUtil.getCreatedId(resp);
getCleanup(bc.providerRealmName()).addUserId(userId);
}
createOrganization("myorg", "myorg.com");
// organization available to broker review profile
loginPage.open(bc.consumerRealmName());
loginPage.loginUsername("tom@myorg.com");
waitForPage(driver, "sign in to", true);
Assert.assertTrue("Driver should be on the provider realm page right now",
driver.getCurrentUrl().contains("/auth/realms/" + bc.providerRealmName() + "/"));
loginPage.login(user.getUsername(), "password");
waitForPage(driver, "update account information", false);
Assert.assertTrue("Driver should be on the consumer realm page right now",
driver.getCurrentUrl().contains("/auth/realms/" + bc.consumerRealmName() + "/"));
Assert.assertTrue(driver.getPageSource().contains("Sign-in to myorg organization"));
}
@Test
public void testOrganizationOnUpdateProfile() {
UserRepresentation user = UserBuilder.create().enabled(true)
.username("tom")
.email("tom@myorg.org")
.password("password")
.firstName("Tom")
.lastName("Brady")
.requiredAction(UserModel.RequiredAction.UPDATE_PROFILE.name())
.build();
try (Response resp = testRealm().users().create(user)) {
String userId = ApiUtil.getCreatedId(resp);
getCleanup(bc.consumerRealmName()).addUserId(userId);
}
createOrganization("myorg", "myorg.com", "myorg.org");
oauth.clientId("broker-app");
loginPage.open(bc.consumerRealmName());
loginPage.loginUsername("tom");
loginPage.login("tom", "password");
waitForPage(driver, "update account information", false);
Assert.assertTrue("Driver should be on the consumer realm page right now",
driver.getCurrentUrl().contains("/auth/realms/" + bc.consumerRealmName() + "/"));
Assert.assertTrue(driver.getPageSource().contains("Sign-in to myorg organization"));
}
@Test
public void testOrganizationAttributes() {
OrganizationRepresentation orgRep = createOrganization("myorg", "myorg.com");
OrganizationResource organization = testRealm().organizations().get(orgRep.getId());
IdentityProviderRepresentation broker = organization.identityProviders().getIdentityProviders().get(0);
broker.getConfig().remove(IdentityProviderRedirectMode.EMAIL_MATCH.getKey());
testRealm().identityProviders().get(broker.getAlias()).update(broker);
// organization available to identity-first login page
loginPage.open(bc.consumerRealmName());
Assert.assertTrue(driver.getPageSource().contains("Sign-in to the realm"));
Assert.assertFalse(loginPage.isPasswordInputPresent());
loginPage.loginUsername("non-user@myorg.com");
Assert.assertTrue(driver.getPageSource().contains("Sign-in to myorg organization"));
for (Entry<String, List<String>> attribute : orgRep.getAttributes().entrySet()) {
Assert.assertTrue(driver.getPageSource().contains("The " + attribute.getKey() + " is " + attribute.getValue()));
}
Assert.assertFalse(loginPage.isPasswordInputPresent());
}
@Test
public void testUserIsMember() {
UserRepresentation user = UserBuilder.create().enabled(true)
.username("tom")
.email("tom@myorg.com")
.password("password")
.firstName("Tom")
.lastName("Brady")
.requiredAction(UserModel.RequiredAction.UPDATE_PROFILE.name())
.build();
try (Response resp = testRealm().users().create(user)) {
String userId = ApiUtil.getCreatedId(resp);
user.setId(userId);
getCleanup(bc.consumerRealmName()).addUserId(userId);
}
OrganizationRepresentation orgRep = createOrganization("myorg", "myorg.com");
OrganizationResource organization = testRealm().organizations().get(orgRep.getId());
IdentityProviderRepresentation broker = organization.identityProviders().getIdentityProviders().get(0);
broker.getConfig().remove(IdentityProviderRedirectMode.EMAIL_MATCH.getKey());
testRealm().identityProviders().get(broker.getAlias()).update(broker);
organization.members().addMember(user.getId()).close();
// organization available to identity-first login page
loginPage.open(bc.consumerRealmName());
loginPage.loginUsername(user.getEmail());
Assert.assertTrue(driver.getPageSource().contains("Sign-in to myorg organization"));
Assert.assertTrue(driver.getPageSource().contains("User is member of " + orgRep.getName()));
Assert.assertTrue(loginPage.isPasswordInputPresent());
}
}

View file

@ -109,6 +109,7 @@ public class OrganizationExportTest extends AbstractOrganizationTest {
List<OrganizationRepresentation> organizations = testRealm().organizations().getAll();
assertEquals(expectedOrganizations.size(), organizations.size());
assertThat(organizations.stream().map(OrganizationRepresentation::getName).toList(), Matchers.containsInAnyOrder(expectedOrganizations.toArray()));
assertThat(organizations.stream().map(OrganizationRepresentation::getAlias).toList(), Matchers.containsInAnyOrder(expectedOrganizations.toArray()));
for (OrganizationRepresentation orgRep : organizations) {
OrganizationResource organization = testRealm().organizations().get(orgRep.getId());