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:
parent
8d0e03a271
commit
a0ad680346
36 changed files with 862 additions and 33 deletions
|
@ -29,6 +29,7 @@ public class OrganizationRepresentation {
|
||||||
|
|
||||||
private String id;
|
private String id;
|
||||||
private String name;
|
private String name;
|
||||||
|
private String alias;
|
||||||
private boolean enabled = true;
|
private boolean enabled = true;
|
||||||
private String description;
|
private String description;
|
||||||
private Map<String, List<String>> attributes;
|
private Map<String, List<String>> attributes;
|
||||||
|
@ -52,6 +53,14 @@ public class OrganizationRepresentation {
|
||||||
return name;
|
return name;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public String getAlias() {
|
||||||
|
return alias;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setAlias(String alias) {
|
||||||
|
this.alias = alias;
|
||||||
|
}
|
||||||
|
|
||||||
public boolean isEnabled() {
|
public boolean isEnabled() {
|
||||||
return this.enabled;
|
return this.enabled;
|
||||||
}
|
}
|
||||||
|
|
|
@ -30,6 +30,10 @@ An organization has the following settings:
|
||||||
Name::
|
Name::
|
||||||
A user-friendly name for the organization. The name is unique within a realm.
|
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::
|
Domains::
|
||||||
A set of one or more domains that belongs to this organization. A domain cannot be shared by different organizations
|
A set of one or more domains that belongs to this organization. A domain cannot be shared by different organizations
|
||||||
within a realm.
|
within a realm.
|
||||||
|
|
|
@ -3164,6 +3164,7 @@ createOrganization=Create organization
|
||||||
domain=Domain
|
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.
|
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
|
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?
|
disableConfirmOrganizationTitle=Disable organization?
|
||||||
disableConfirmOrganization=Are you sure you want to disable this organization?
|
disableConfirmOrganization=Are you sure you want to disable this organization?
|
||||||
memberList=Member list
|
memberList=Member list
|
||||||
|
|
|
@ -9,6 +9,10 @@ import { useTranslation } from "react-i18next";
|
||||||
import { AttributeForm } from "../components/key-value-form/AttributeForm";
|
import { AttributeForm } from "../components/key-value-form/AttributeForm";
|
||||||
import { MultiLineInput } from "../components/multi-line-input/MultiLineInput";
|
import { MultiLineInput } from "../components/multi-line-input/MultiLineInput";
|
||||||
import { keyValueToArray } from "../components/key-value-form/key-value-convert";
|
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 &
|
export type OrganizationFormType = AttributeForm &
|
||||||
Omit<OrganizationRepresentation, "domains" | "attributes"> & {
|
Omit<OrganizationRepresentation, "domains" | "attributes"> & {
|
||||||
|
@ -25,6 +29,19 @@ export const convertToOrg = (
|
||||||
|
|
||||||
export const OrganizationForm = () => {
|
export const OrganizationForm = () => {
|
||||||
const { t } = useTranslation();
|
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 (
|
return (
|
||||||
<>
|
<>
|
||||||
<TextControl
|
<TextControl
|
||||||
|
@ -32,6 +49,12 @@ export const OrganizationForm = () => {
|
||||||
name="name"
|
name="name"
|
||||||
rules={{ required: t("required") }}
|
rules={{ required: t("required") }}
|
||||||
/>
|
/>
|
||||||
|
<TextControl
|
||||||
|
label={t("alias")}
|
||||||
|
name="alias"
|
||||||
|
labelIcon={t("organizationAliasHelp")}
|
||||||
|
isDisabled={!isEditable}
|
||||||
|
/>
|
||||||
<FormGroup
|
<FormGroup
|
||||||
label={t("domain")}
|
label={t("domain")}
|
||||||
fieldId="domain"
|
fieldId="domain"
|
||||||
|
|
|
@ -34,6 +34,7 @@ public class CachedOrganization extends AbstractRevisioned implements InRealm {
|
||||||
|
|
||||||
private final RealmModel realm;
|
private final RealmModel realm;
|
||||||
private final String name;
|
private final String name;
|
||||||
|
private final String alias;
|
||||||
private final String description;
|
private final String description;
|
||||||
private final boolean enabled;
|
private final boolean enabled;
|
||||||
private final LazyLoader<OrganizationModel, MultivaluedHashMap<String, String>> attributes;
|
private final LazyLoader<OrganizationModel, MultivaluedHashMap<String, String>> attributes;
|
||||||
|
@ -44,6 +45,7 @@ public class CachedOrganization extends AbstractRevisioned implements InRealm {
|
||||||
super(revision, organization.getId());
|
super(revision, organization.getId());
|
||||||
this.realm = realm;
|
this.realm = realm;
|
||||||
this.name = organization.getName();
|
this.name = organization.getName();
|
||||||
|
this.alias = organization.getAlias();
|
||||||
this.description = organization.getDescription();
|
this.description = organization.getDescription();
|
||||||
this.enabled = organization.isEnabled();
|
this.enabled = organization.isEnabled();
|
||||||
this.attributes = new DefaultLazyLoader<>(orgModel -> new MultivaluedHashMap<>(orgModel.getAttributes()), MultivaluedHashMap::new);
|
this.attributes = new DefaultLazyLoader<>(orgModel -> new MultivaluedHashMap<>(orgModel.getAttributes()), MultivaluedHashMap::new);
|
||||||
|
@ -64,6 +66,10 @@ public class CachedOrganization extends AbstractRevisioned implements InRealm {
|
||||||
return name;
|
return name;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public String getAlias() {
|
||||||
|
return alias;
|
||||||
|
}
|
||||||
|
|
||||||
public String getDescription() {
|
public String getDescription() {
|
||||||
return description;
|
return description;
|
||||||
}
|
}
|
||||||
|
|
|
@ -48,9 +48,9 @@ public class InfinispanOrganizationProvider implements OrganizationProvider {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public OrganizationModel create(String name) {
|
public OrganizationModel create(String name, String alias) {
|
||||||
registerCountInvalidation();
|
registerCountInvalidation();
|
||||||
return orgDelegate.create(name);
|
return orgDelegate.create(name, alias);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
|
@ -86,6 +86,18 @@ public class OrganizationAdapter implements OrganizationModel {
|
||||||
updated.setName(name);
|
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
|
@Override
|
||||||
public boolean isEnabled() {
|
public boolean isEnabled() {
|
||||||
if (isUpdated()) return updated.isEnabled();
|
if (isUpdated()) return updated.isEnabled();
|
||||||
|
@ -145,4 +157,17 @@ public class OrganizationAdapter implements OrganizationModel {
|
||||||
return delegate.isManagedMember(this, user);
|
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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -55,6 +55,9 @@ public class OrganizationEntity {
|
||||||
@Column(name = "NAME")
|
@Column(name = "NAME")
|
||||||
private String name;
|
private String name;
|
||||||
|
|
||||||
|
@Column(name = "ALIAS")
|
||||||
|
private String alias;
|
||||||
|
|
||||||
@Column(name = "ENABLED")
|
@Column(name = "ENABLED")
|
||||||
private boolean enabled;
|
private boolean enabled;
|
||||||
|
|
||||||
|
@ -82,6 +85,14 @@ public class OrganizationEntity {
|
||||||
this.name = name;
|
this.name = name;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public String getAlias() {
|
||||||
|
return alias;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setAlias(String alias) {
|
||||||
|
this.alias = alias;
|
||||||
|
}
|
||||||
|
|
||||||
public boolean isEnabled() {
|
public boolean isEnabled() {
|
||||||
return this.enabled;
|
return this.enabled;
|
||||||
}
|
}
|
||||||
|
|
|
@ -70,15 +70,23 @@ public class JpaOrganizationProvider implements OrganizationProvider {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public OrganizationModel create(String name) {
|
public OrganizationModel create(String name, String alias) {
|
||||||
if (StringUtil.isBlank(name)) {
|
if (StringUtil.isBlank(name)) {
|
||||||
throw new ModelValidationException("Name can not be null");
|
throw new ModelValidationException("Name can not be null");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (StringUtil.isBlank(alias)) {
|
||||||
|
alias = name;
|
||||||
|
}
|
||||||
|
|
||||||
if (getByName(name) != null) {
|
if (getByName(name) != null) {
|
||||||
throw new ModelDuplicateException("A organization with the same name already exists.");
|
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();
|
RealmModel realm = getRealm();
|
||||||
OrganizationAdapter adapter = new OrganizationAdapter(realm, this);
|
OrganizationAdapter adapter = new OrganizationAdapter(realm, this);
|
||||||
|
|
||||||
|
@ -88,6 +96,7 @@ public class JpaOrganizationProvider implements OrganizationProvider {
|
||||||
|
|
||||||
adapter.setGroupId(group.getId());
|
adapter.setGroupId(group.getId());
|
||||||
adapter.setName(name);
|
adapter.setName(name);
|
||||||
|
adapter.setAlias(alias);
|
||||||
adapter.setEnabled(true);
|
adapter.setEnabled(true);
|
||||||
|
|
||||||
em.persist(adapter.getEntity());
|
em.persist(adapter.getEntity());
|
||||||
|
@ -224,7 +233,13 @@ public class JpaOrganizationProvider implements OrganizationProvider {
|
||||||
predicates.add(builder.equal(org.get("groupId"), group.get("id")));
|
predicates.add(builder.equal(org.get("groupId"), group.get("id")));
|
||||||
|
|
||||||
for (Map.Entry<String, String> entry : attributes.entrySet()) {
|
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");
|
Join<GroupEntity, GroupAttributeEntity> groupJoin = group.join("attributes");
|
||||||
Predicate attrNamePredicate = builder.equal(groupJoin.get("name"), entry.getKey());
|
Predicate attrNamePredicate = builder.equal(groupJoin.get("name"), entry.getKey());
|
||||||
Predicate attrValuePredicate = builder.equal(groupJoin.get("value"), entry.getValue());
|
Predicate attrValuePredicate = builder.equal(groupJoin.get("value"), entry.getValue());
|
||||||
|
|
|
@ -93,6 +93,25 @@ public final class OrganizationAdapter implements OrganizationModel, JpaModel<Or
|
||||||
return entity.getName();
|
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
|
@Override
|
||||||
public boolean isEnabled() {
|
public boolean isEnabled() {
|
||||||
return provider.isEnabled() && entity.isEnabled();
|
return provider.isEnabled() && entity.isEnabled();
|
||||||
|
|
|
@ -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>
|
|
@ -82,5 +82,6 @@
|
||||||
<include file="META-INF/jpa-changelog-24.0.0.xml"/>
|
<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-24.0.2.xml"/>
|
||||||
<include file="META-INF/jpa-changelog-25.0.0.xml"/>
|
<include file="META-INF/jpa-changelog-25.0.0.xml"/>
|
||||||
|
<include file="META-INF/jpa-changelog-26.0.0.xml"/>
|
||||||
|
|
||||||
</databaseChangeLog>
|
</databaseChangeLog>
|
||||||
|
|
|
@ -265,6 +265,7 @@ public class ExportUtils {
|
||||||
OrganizationRepresentation org = new OrganizationRepresentation();
|
OrganizationRepresentation org = new OrganizationRepresentation();
|
||||||
|
|
||||||
org.setName(m.getName());
|
org.setName(m.getName());
|
||||||
|
org.setAlias(m.getAlias());
|
||||||
org.setEnabled(m.isEnabled());
|
org.setEnabled(m.isEnabled());
|
||||||
org.setDescription(m.getDescription());
|
org.setDescription(m.getDescription());
|
||||||
m.getDomains().map(d -> {
|
m.getDomains().map(d -> {
|
||||||
|
|
|
@ -1589,7 +1589,8 @@ public class DefaultExportImportManager implements ExportImportManager {
|
||||||
OrganizationProvider provider = session.getProvider(OrganizationProvider.class);
|
OrganizationProvider provider = session.getProvider(OrganizationProvider.class);
|
||||||
|
|
||||||
for (OrganizationRepresentation orgRep : Optional.ofNullable(rep.getOrganizations()).orElse(Collections.emptyList())) {
|
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()));
|
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())) {
|
for (IdentityProviderRepresentation identityProvider : Optional.ofNullable(orgRep.getIdentityProviders()).orElse(Collections.emptyList())) {
|
||||||
|
|
|
@ -28,6 +28,7 @@ public interface OrganizationModel {
|
||||||
String ORGANIZATION_NAME_ATTRIBUTE = "kc.org.name";
|
String ORGANIZATION_NAME_ATTRIBUTE = "kc.org.name";
|
||||||
String ORGANIZATION_DOMAIN_ATTRIBUTE = "kc.org.domain";
|
String ORGANIZATION_DOMAIN_ATTRIBUTE = "kc.org.domain";
|
||||||
String BROKER_PUBLIC = "kc.org.broker.public";
|
String BROKER_PUBLIC = "kc.org.broker.public";
|
||||||
|
String ALIAS = "alias";
|
||||||
|
|
||||||
enum IdentityProviderRedirectMode {
|
enum IdentityProviderRedirectMode {
|
||||||
EMAIL_MATCH("kc.org.broker.redirect.mode.email-matches");
|
EMAIL_MATCH("kc.org.broker.redirect.mode.email-matches");
|
||||||
|
@ -53,6 +54,10 @@ public interface OrganizationModel {
|
||||||
|
|
||||||
String getName();
|
String getName();
|
||||||
|
|
||||||
|
String getAlias();
|
||||||
|
|
||||||
|
void setAlias(String alias);
|
||||||
|
|
||||||
boolean isEnabled();
|
boolean isEnabled();
|
||||||
|
|
||||||
void setEnabled(boolean enabled);
|
void setEnabled(boolean enabled);
|
||||||
|
|
|
@ -34,10 +34,11 @@ public interface OrganizationProvider extends Provider {
|
||||||
* Creates a new organization with given {@code name} to the realm.
|
* Creates a new organization with given {@code name} to the realm.
|
||||||
* The internal ID of the organization will be created automatically.
|
* The internal ID of the organization will be created automatically.
|
||||||
* @param name String name of the organization.
|
* @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.
|
* @return Model of the created organization.
|
||||||
*/
|
*/
|
||||||
OrganizationModel create(String name);
|
OrganizationModel create(String name, String alias);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns a {@link OrganizationModel} by its {@code id};
|
* Returns a {@link OrganizationModel} by its {@code id};
|
||||||
|
|
|
@ -97,6 +97,8 @@ public class UpdateProfile implements RequiredActionProvider, RequiredActionFact
|
||||||
form = form.setFormData(formData);
|
form = form.setFormData(formData);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
form.setUser(context.getUser());
|
||||||
|
|
||||||
return form.createResponse(getResponseAction());
|
return form.createResponse(getResponseAction());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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.LoginBean;
|
||||||
import org.keycloak.forms.login.freemarker.model.LogoutConfirmBean;
|
import org.keycloak.forms.login.freemarker.model.LogoutConfirmBean;
|
||||||
import org.keycloak.forms.login.freemarker.model.OAuthGrantBean;
|
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.ProfileBean;
|
||||||
import org.keycloak.forms.login.freemarker.model.RealmBean;
|
import org.keycloak.forms.login.freemarker.model.RealmBean;
|
||||||
import org.keycloak.forms.login.freemarker.model.RecoveryAuthnCodeInputLoginBean;
|
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.Constants;
|
||||||
import org.keycloak.models.IdentityProviderModel;
|
import org.keycloak.models.IdentityProviderModel;
|
||||||
import org.keycloak.models.KeycloakSession;
|
import org.keycloak.models.KeycloakSession;
|
||||||
|
import org.keycloak.models.OrganizationModel;
|
||||||
import org.keycloak.models.RealmModel;
|
import org.keycloak.models.RealmModel;
|
||||||
import org.keycloak.models.UserModel;
|
import org.keycloak.models.UserModel;
|
||||||
import org.keycloak.models.utils.FormMessage;
|
import org.keycloak.models.utils.FormMessage;
|
||||||
|
@ -101,6 +103,7 @@ import java.util.Properties;
|
||||||
import java.util.function.Function;
|
import java.util.function.Function;
|
||||||
|
|
||||||
import static org.keycloak.models.UserModel.RequiredAction.UPDATE_PASSWORD;
|
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>
|
* @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));
|
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) {
|
if (realm != null && user != null && session != null) {
|
||||||
attributes.put("authenticatorConfigured", new AuthenticatorConfiguredMethod(realm, user, session));
|
attributes.put("authenticatorConfigured", new AuthenticatorConfiguredMethod(realm, user, session));
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -31,6 +31,7 @@ import jakarta.ws.rs.Produces;
|
||||||
import jakarta.ws.rs.QueryParam;
|
import jakarta.ws.rs.QueryParam;
|
||||||
import jakarta.ws.rs.core.MediaType;
|
import jakarta.ws.rs.core.MediaType;
|
||||||
import jakarta.ws.rs.core.Response;
|
import jakarta.ws.rs.core.Response;
|
||||||
|
import jakarta.ws.rs.core.Response.Status;
|
||||||
import jakarta.ws.rs.ext.Provider;
|
import jakarta.ws.rs.ext.Provider;
|
||||||
import org.eclipse.microprofile.openapi.annotations.Operation;
|
import org.eclipse.microprofile.openapi.annotations.Operation;
|
||||||
import org.eclipse.microprofile.openapi.annotations.extensions.Extension;
|
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.eclipse.microprofile.openapi.annotations.tags.Tag;
|
||||||
import org.jboss.resteasy.reactive.NoCache;
|
import org.jboss.resteasy.reactive.NoCache;
|
||||||
import org.keycloak.models.KeycloakSession;
|
import org.keycloak.models.KeycloakSession;
|
||||||
|
import org.keycloak.models.ModelDuplicateException;
|
||||||
import org.keycloak.models.ModelValidationException;
|
import org.keycloak.models.ModelValidationException;
|
||||||
import org.keycloak.models.OrganizationModel;
|
import org.keycloak.models.OrganizationModel;
|
||||||
import org.keycloak.organization.OrganizationProvider;
|
import org.keycloak.organization.OrganizationProvider;
|
||||||
|
@ -90,13 +92,15 @@ public class OrganizationsResource {
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
OrganizationModel model = provider.create(organization.getName());
|
OrganizationModel model = provider.create(organization.getName(), organization.getAlias());
|
||||||
|
|
||||||
Organizations.toModel(organization, model);
|
Organizations.toModel(organization, model);
|
||||||
|
|
||||||
return Response.created(session.getContext().getUri().getAbsolutePathBuilder().path(model.getId()).build()).build();
|
return Response.created(session.getContext().getUri().getAbsolutePathBuilder().path(model.getId()).build()).build();
|
||||||
} catch (ModelValidationException mve) {
|
} catch (ModelValidationException mve) {
|
||||||
throw ErrorResponse.error(mve.getMessage(), Response.Status.BAD_REQUEST);
|
throw ErrorResponse.error(mve.getMessage(), Response.Status.BAD_REQUEST);
|
||||||
|
} catch (ModelDuplicateException mve) {
|
||||||
|
throw ErrorResponse.error(mve.getMessage(), Status.CONFLICT);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -17,6 +17,7 @@
|
||||||
|
|
||||||
package org.keycloak.organization.authentication.authenticators.browser;
|
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.isEnabledAndOrganizationsPresent;
|
||||||
import static org.keycloak.organization.utils.Organizations.resolveBroker;
|
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.LoginFormsProvider;
|
||||||
import org.keycloak.forms.login.freemarker.model.AuthenticationContextBean;
|
import org.keycloak.forms.login.freemarker.model.AuthenticationContextBean;
|
||||||
import org.keycloak.forms.login.freemarker.model.IdentityProviderBean;
|
import org.keycloak.forms.login.freemarker.model.IdentityProviderBean;
|
||||||
|
import org.keycloak.forms.login.freemarker.model.OrganizationBean;
|
||||||
import org.keycloak.http.HttpRequest;
|
import org.keycloak.http.HttpRequest;
|
||||||
import org.keycloak.models.IdentityProviderModel;
|
import org.keycloak.models.IdentityProviderModel;
|
||||||
import org.keycloak.models.KeycloakSession;
|
import org.keycloak.models.KeycloakSession;
|
||||||
|
@ -77,6 +79,14 @@ public class OrganizationAuthenticator extends IdentityProviderAuthenticator {
|
||||||
return;
|
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();
|
RealmModel realm = context.getRealm();
|
||||||
UserModel user = session.users().getUserByEmail(realm, username);
|
UserModel user = session.users().getUserByEmail(realm, username);
|
||||||
|
|
||||||
|
@ -91,6 +101,12 @@ public class OrganizationAuthenticator extends IdentityProviderAuthenticator {
|
||||||
|
|
||||||
if (broker.isEmpty()) {
|
if (broker.isEmpty()) {
|
||||||
// not a managed member, continue with the regular flow
|
// 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();
|
context.attempted();
|
||||||
} else if (broker.size() == 1) {
|
} else if (broker.size() == 1) {
|
||||||
// user is a managed member and associated with a broker, redirect automatically
|
// user is a managed member and associated with a broker, redirect automatically
|
||||||
|
@ -100,9 +116,6 @@ public class OrganizationAuthenticator extends IdentityProviderAuthenticator {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
OrganizationProvider provider = getOrganizationProvider();
|
|
||||||
OrganizationModel organization = provider.getByDomainName(emailDomain);
|
|
||||||
|
|
||||||
if (organization == null || !organization.isEnabled()) {
|
if (organization == null || !organization.isEnabled()) {
|
||||||
// request does not map to any organization, go to the next step/sub-flow
|
// request does not map to any organization, go to the next step/sub-flow
|
||||||
context.attempted();
|
context.attempted();
|
||||||
|
@ -173,20 +186,6 @@ public class OrganizationAuthenticator extends IdentityProviderAuthenticator {
|
||||||
context.challenge(form.createLoginUsername());
|
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
|
@Override
|
||||||
public boolean configuredFor(KeycloakSession session, RealmModel realm, UserModel user) {
|
public boolean configuredFor(KeycloakSession session, RealmModel realm, UserModel user) {
|
||||||
return realm.isOrganizationsEnabled();
|
return realm.isOrganizationsEnabled();
|
||||||
|
|
|
@ -91,7 +91,7 @@ public class OrganizationMembershipMapper extends AbstractOIDCProtocolMapper imp
|
||||||
}
|
}
|
||||||
|
|
||||||
Map<String, Map<String, Object>> claim = new HashMap<>();
|
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);
|
token.getOtherClaims().put(OAuth2Constants.ORGANIZATION, claim);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -74,7 +74,7 @@ public class OrganizationMembershipMapper extends AbstractSAMLProtocolMapper imp
|
||||||
AttributeType attribute = new AttributeType(ORGANIZATION_ATTRIBUTE_NAME);
|
AttributeType attribute = new AttributeType(ORGANIZATION_ATTRIBUTE_NAME);
|
||||||
attribute.setFriendlyName(ORGANIZATION_ATTRIBUTE_NAME);
|
attribute.setFriendlyName(ORGANIZATION_ATTRIBUTE_NAME);
|
||||||
attribute.setNameFormat(JBossSAMLURIConstants.ATTRIBUTE_FORMAT_BASIC.get());
|
attribute.setNameFormat(JBossSAMLURIConstants.ATTRIBUTE_FORMAT_BASIC.get());
|
||||||
attribute.addAttributeValue(organization.getName());
|
attribute.addAttributeValue(organization.getAlias());
|
||||||
attributeStatement.addAttribute(new AttributeStatementType.ASTChoiceType(attribute));
|
attributeStatement.addAttribute(new AttributeStatementType.ASTChoiceType(attribute));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -146,6 +146,7 @@ public class Organizations {
|
||||||
|
|
||||||
rep.setId(model.getId());
|
rep.setId(model.getId());
|
||||||
rep.setName(model.getName());
|
rep.setName(model.getName());
|
||||||
|
rep.setAlias(model.getAlias());
|
||||||
rep.setEnabled(model.isEnabled());
|
rep.setEnabled(model.isEnabled());
|
||||||
rep.setDescription(model.getDescription());
|
rep.setDescription(model.getDescription());
|
||||||
rep.setAttributes(model.getAttributes());
|
rep.setAttributes(model.getAttributes());
|
||||||
|
@ -168,6 +169,7 @@ public class Organizations {
|
||||||
}
|
}
|
||||||
|
|
||||||
model.setName(rep.getName());
|
model.setName(rep.getName());
|
||||||
|
model.setAlias(rep.getAlias());
|
||||||
model.setEnabled(rep.isEnabled());
|
model.setEnabled(rep.isEnabled());
|
||||||
model.setDescription(rep.getDescription());
|
model.setDescription(rep.getDescription());
|
||||||
model.setAttributes(rep.getAttributes());
|
model.setAttributes(rep.getAttributes());
|
||||||
|
@ -194,4 +196,41 @@ public class Organizations {
|
||||||
|
|
||||||
return TokenVerifier.create(tokenFromQuery, InviteOrgActionToken.class).getToken();
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,5 +5,9 @@
|
||||||
}, {
|
}, {
|
||||||
"name" : "incorrect",
|
"name" : "incorrect",
|
||||||
"types": [ "admin" ]
|
"types": [ "admin" ]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name" : "organization",
|
||||||
|
"types": [ "login" ]
|
||||||
}]
|
}]
|
||||||
}
|
}
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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
|
|
@ -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("account"), "base", "keycloak.v3", "custom-account-provider");
|
||||||
Assert.assertNames(info.getThemes().get("admin"), "base", "keycloak.v2");
|
Assert.assertNames(info.getThemes().get("admin"), "base", "keycloak.v2");
|
||||||
Assert.assertNames(info.getThemes().get("email"), "base", "keycloak");
|
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");
|
Assert.assertNames(info.getThemes().get("welcome"), "keycloak");
|
||||||
|
|
||||||
assertNotNull(info.getEnums());
|
assertNotNull(info.getEnums());
|
||||||
|
|
|
@ -133,6 +133,7 @@ public abstract class AbstractOrganizationTest extends AbstractAdminTest {
|
||||||
protected OrganizationRepresentation createRepresentation(String name, String... orgDomains) {
|
protected OrganizationRepresentation createRepresentation(String name, String... orgDomains) {
|
||||||
OrganizationRepresentation org = new OrganizationRepresentation();
|
OrganizationRepresentation org = new OrganizationRepresentation();
|
||||||
org.setName(name);
|
org.setName(name);
|
||||||
|
org.setAlias(name);
|
||||||
|
|
||||||
for (String orgDomain : orgDomains) {
|
for (String orgDomain : orgDomains) {
|
||||||
OrganizationDomainRepresentation domainRep = new OrganizationDomainRepresentation();
|
OrganizationDomainRepresentation domainRep = new OrganizationDomainRepresentation();
|
||||||
|
|
|
@ -45,11 +45,13 @@ import java.io.IOException;
|
||||||
import java.util.stream.IntStream;
|
import java.util.stream.IntStream;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
import org.keycloak.admin.client.resource.OrganizationResource;
|
import org.keycloak.admin.client.resource.OrganizationResource;
|
||||||
|
import org.keycloak.admin.client.resource.OrganizationsResource;
|
||||||
import org.keycloak.admin.client.resource.RealmResource;
|
import org.keycloak.admin.client.resource.RealmResource;
|
||||||
import org.keycloak.common.Profile.Feature;
|
import org.keycloak.common.Profile.Feature;
|
||||||
import org.keycloak.models.OrganizationModel;
|
import org.keycloak.models.OrganizationModel;
|
||||||
import org.keycloak.models.utils.KeycloakModelUtils;
|
import org.keycloak.models.utils.KeycloakModelUtils;
|
||||||
import org.keycloak.organization.OrganizationProvider;
|
import org.keycloak.organization.OrganizationProvider;
|
||||||
|
import org.keycloak.representations.idm.ErrorRepresentation;
|
||||||
import org.keycloak.representations.idm.IdentityProviderRepresentation;
|
import org.keycloak.representations.idm.IdentityProviderRepresentation;
|
||||||
import org.keycloak.representations.idm.OrganizationDomainRepresentation;
|
import org.keycloak.representations.idm.OrganizationDomainRepresentation;
|
||||||
import org.keycloak.representations.idm.OrganizationRepresentation;
|
import org.keycloak.representations.idm.OrganizationRepresentation;
|
||||||
|
@ -80,6 +82,7 @@ public class OrganizationTest extends AbstractOrganizationTest {
|
||||||
OrganizationRepresentation existing = organization.toRepresentation();
|
OrganizationRepresentation existing = organization.toRepresentation();
|
||||||
assertEquals(expected.getId(), existing.getId());
|
assertEquals(expected.getId(), existing.getId());
|
||||||
assertEquals(expected.getName(), existing.getName());
|
assertEquals(expected.getName(), existing.getName());
|
||||||
|
assertEquals(expected.getAlias(), existing.getAlias());
|
||||||
assertEquals(1, existing.getDomains().size());
|
assertEquals(1, existing.getDomains().size());
|
||||||
assertThat(existing.isEnabled(), is(false));
|
assertThat(existing.isEnabled(), is(false));
|
||||||
assertThat(existing.getDescription(), notNullValue());
|
assertThat(existing.getDescription(), notNullValue());
|
||||||
|
@ -463,4 +466,43 @@ public class OrganizationTest extends AbstractOrganizationTest {
|
||||||
assertEquals(9, orgProvider.count());
|
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());
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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());
|
||||||
|
}
|
||||||
|
}
|
|
@ -109,6 +109,7 @@ public class OrganizationExportTest extends AbstractOrganizationTest {
|
||||||
List<OrganizationRepresentation> organizations = testRealm().organizations().getAll();
|
List<OrganizationRepresentation> organizations = testRealm().organizations().getAll();
|
||||||
assertEquals(expectedOrganizations.size(), organizations.size());
|
assertEquals(expectedOrganizations.size(), organizations.size());
|
||||||
assertThat(organizations.stream().map(OrganizationRepresentation::getName).toList(), Matchers.containsInAnyOrder(expectedOrganizations.toArray()));
|
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) {
|
for (OrganizationRepresentation orgRep : organizations) {
|
||||||
OrganizationResource organization = testRealm().organizations().get(orgRep.getId());
|
OrganizationResource organization = testRealm().organizations().get(orgRep.getId());
|
||||||
|
|
Loading…
Reference in a new issue