[Organizations] Allow orgs to define the redirect URL after user registers or accepts invitation link
Closes #33201 Signed-off-by: vramik <vramik@redhat.com>
This commit is contained in:
parent
d07d5ebf1a
commit
c1653448f3
29 changed files with 297 additions and 33 deletions
|
@ -32,6 +32,7 @@ public class OrganizationRepresentation {
|
||||||
private String alias;
|
private String alias;
|
||||||
private boolean enabled = true;
|
private boolean enabled = true;
|
||||||
private String description;
|
private String description;
|
||||||
|
private String redirectUrl;
|
||||||
private Map<String, List<String>> attributes;
|
private Map<String, List<String>> attributes;
|
||||||
private Set<OrganizationDomainRepresentation> domains;
|
private Set<OrganizationDomainRepresentation> domains;
|
||||||
private List<MemberRepresentation> members;
|
private List<MemberRepresentation> members;
|
||||||
|
@ -77,6 +78,14 @@ public class OrganizationRepresentation {
|
||||||
this.description = description;
|
this.description = description;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public String getRedirectUrl() {
|
||||||
|
return redirectUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setRedirectUrl(String redirectUrl) {
|
||||||
|
this.redirectUrl = redirectUrl;
|
||||||
|
}
|
||||||
|
|
||||||
public Map<String, List<String>> getAttributes() {
|
public Map<String, List<String>> getAttributes() {
|
||||||
return attributes;
|
return attributes;
|
||||||
}
|
}
|
||||||
|
|
Binary file not shown.
Before Width: | Height: | Size: 58 KiB After Width: | Height: | Size: 53 KiB |
Binary file not shown.
Before Width: | Height: | Size: 80 KiB After Width: | Height: | Size: 72 KiB |
Binary file not shown.
Before Width: | Height: | Size: 75 KiB After Width: | Height: | Size: 67 KiB |
|
@ -41,9 +41,11 @@ 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::
|
Alias::
|
||||||
|
|
||||||
An alias for this organization, used to reference the organization internally. The alias is unique within a realm and must be URL-friendly, so characters not usually allowed in URLs will not be allowed in the alias. If not set, {project_name} will attempt to use the name as the alias. If the name is not URL-friendly, you will get an error and will be asked to specify an alias. Once defined, the alias cannot be changed afterwards.
|
An alias for this organization, used to reference the organization internally. The alias is unique within a realm and must be URL-friendly, so characters not usually allowed in URLs will not be allowed in the alias. If not set, {project_name} will attempt to use the name as the alias. If the name is not URL-friendly, you will get an error and will be asked to specify an alias. Once defined, the alias cannot be changed afterwards.
|
||||||
|
|
||||||
|
Redirect URL::
|
||||||
|
After completing registration or accepting an invitation to the organization sent via email, the user is automatically redirected to the specified redirect url. If left empty, the user will be redirected to the account console by default.
|
||||||
|
|
||||||
Domains::
|
Domains::
|
||||||
A set of one or more domains that belongs to this organization. A domain cannot be shared by different organizations within a realm.
|
A set of one or more domains that belongs to this organization. A domain cannot be shared by different organizations within a realm.
|
||||||
|
|
||||||
|
|
|
@ -3175,6 +3175,8 @@ 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.
|
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.
|
||||||
|
organizationRedirectUrlHelp=Automatically redirect the user after completing registration or accepting an invitation to the organization. If left empty, the user will be redirected to the account console by default.
|
||||||
|
redirectUrl=Redirect URL
|
||||||
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
|
||||||
|
@ -3259,4 +3261,4 @@ eventTypes.REMOVE_CREDENTIAL_ERROR.description=Remove credential error
|
||||||
groupDuplicated=Group duplicated
|
groupDuplicated=Group duplicated
|
||||||
duplicateAGroup=Duplicate group
|
duplicateAGroup=Duplicate group
|
||||||
couldNotFetchClientRoleMappings=Could not fetch client role mappings\: {{error}}
|
couldNotFetchClientRoleMappings=Could not fetch client role mappings\: {{error}}
|
||||||
duplicateGroupWarning=Duplication of groups with a large number of subgroups is not supported. Please ensure that the group you are duplicating does not have a large number of subgroups.
|
duplicateGroupWarning=Duplication of groups with a large number of subgroups is not supported. Please ensure that the group you are duplicating does not have a large number of subgroups.
|
||||||
|
|
|
@ -72,6 +72,11 @@ export const OrganizationForm = ({
|
||||||
addButtonLabel="addDomain"
|
addButtonLabel="addDomain"
|
||||||
/>
|
/>
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
|
<TextControl
|
||||||
|
label={t("redirectUrl")}
|
||||||
|
name="redirectUrl"
|
||||||
|
labelIcon={t("organizationRedirectUrlHelp")}
|
||||||
|
/>
|
||||||
<TextAreaControl name="description" label={t("description")} />
|
<TextAreaControl name="description" label={t("description")} />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
|
@ -4,6 +4,7 @@ export default interface OrganizationRepresentation {
|
||||||
id?: string;
|
id?: string;
|
||||||
name?: string;
|
name?: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
|
redirectUrl?: string;
|
||||||
enabled?: boolean;
|
enabled?: boolean;
|
||||||
attributes?: Record<string, string[]>;
|
attributes?: Record<string, string[]>;
|
||||||
domains?: OrganizationDomainRepresentation[];
|
domains?: OrganizationDomainRepresentation[];
|
||||||
|
|
|
@ -36,6 +36,7 @@ public class CachedOrganization extends AbstractRevisioned implements InRealm {
|
||||||
private final String name;
|
private final String name;
|
||||||
private final String alias;
|
private final String alias;
|
||||||
private final String description;
|
private final String description;
|
||||||
|
private final String redirectUrl;
|
||||||
private final boolean enabled;
|
private final boolean enabled;
|
||||||
private final LazyLoader<OrganizationModel, MultivaluedHashMap<String, String>> attributes;
|
private final LazyLoader<OrganizationModel, MultivaluedHashMap<String, String>> attributes;
|
||||||
private final Set<OrganizationDomainModel> domains;
|
private final Set<OrganizationDomainModel> domains;
|
||||||
|
@ -47,6 +48,7 @@ public class CachedOrganization extends AbstractRevisioned implements InRealm {
|
||||||
this.name = organization.getName();
|
this.name = organization.getName();
|
||||||
this.alias = organization.getAlias();
|
this.alias = organization.getAlias();
|
||||||
this.description = organization.getDescription();
|
this.description = organization.getDescription();
|
||||||
|
this.redirectUrl = organization.getRedirectUrl();
|
||||||
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);
|
||||||
this.domains = organization.getDomains().collect(Collectors.toSet());
|
this.domains = organization.getDomains().collect(Collectors.toSet());
|
||||||
|
@ -70,6 +72,10 @@ public class CachedOrganization extends AbstractRevisioned implements InRealm {
|
||||||
return description;
|
return description;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public String getRedirectUrl() {
|
||||||
|
return redirectUrl;
|
||||||
|
}
|
||||||
|
|
||||||
public boolean isEnabled() {
|
public boolean isEnabled() {
|
||||||
return enabled;
|
return enabled;
|
||||||
}
|
}
|
||||||
|
|
|
@ -121,6 +121,18 @@ public class OrganizationAdapter implements OrganizationModel {
|
||||||
updated.setDescription(description);
|
updated.setDescription(description);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getRedirectUrl() {
|
||||||
|
if (isUpdated()) return updated.getRedirectUrl();
|
||||||
|
return cached.getRedirectUrl();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setRedirectUrl(String redirectUrl) {
|
||||||
|
getDelegateForUpdate();
|
||||||
|
updated.setRedirectUrl(redirectUrl);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Map<String, List<String>> getAttributes() {
|
public Map<String, List<String>> getAttributes() {
|
||||||
if (isUpdated()) return updated.getAttributes();
|
if (isUpdated()) return updated.getAttributes();
|
||||||
|
|
|
@ -31,6 +31,7 @@ import jakarta.persistence.NamedQueries;
|
||||||
import jakarta.persistence.NamedQuery;
|
import jakarta.persistence.NamedQuery;
|
||||||
import jakarta.persistence.OneToMany;
|
import jakarta.persistence.OneToMany;
|
||||||
import jakarta.persistence.Table;
|
import jakarta.persistence.Table;
|
||||||
|
import org.keycloak.utils.StringUtil;
|
||||||
|
|
||||||
@Table(name="ORG")
|
@Table(name="ORG")
|
||||||
@Entity
|
@Entity
|
||||||
|
@ -66,6 +67,9 @@ public class OrganizationEntity {
|
||||||
@Column(name = "DESCRIPTION")
|
@Column(name = "DESCRIPTION")
|
||||||
private String description;
|
private String description;
|
||||||
|
|
||||||
|
@Column(name = "REDIRECT_URL")
|
||||||
|
private String redirectUrl;
|
||||||
|
|
||||||
@Column(name = "REALM_ID")
|
@Column(name = "REALM_ID")
|
||||||
private String realmId;
|
private String realmId;
|
||||||
|
|
||||||
|
@ -111,6 +115,17 @@ public class OrganizationEntity {
|
||||||
this.description = description;
|
this.description = description;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public String getRedirectUrl() {
|
||||||
|
return redirectUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setRedirectUrl(String redirectUrl) {
|
||||||
|
if (StringUtil.isNullOrEmpty(redirectUrl)) {
|
||||||
|
redirectUrl = null;
|
||||||
|
}
|
||||||
|
this.redirectUrl = redirectUrl;
|
||||||
|
}
|
||||||
|
|
||||||
public String getRealmId() {
|
public String getRealmId() {
|
||||||
return realmId;
|
return realmId;
|
||||||
}
|
}
|
||||||
|
|
|
@ -126,6 +126,16 @@ public final class OrganizationAdapter implements OrganizationModel, JpaModel<Or
|
||||||
entity.setDescription(description);
|
entity.setDescription(description);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getRedirectUrl() {
|
||||||
|
return entity.getRedirectUrl();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setRedirectUrl(String redirectUrl) {
|
||||||
|
entity.setRedirectUrl(redirectUrl);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void setAttributes(Map<String, List<String>> attributes) {
|
public void setAttributes(Map<String, List<String>> attributes) {
|
||||||
if (attributes == null) {
|
if (attributes == null) {
|
||||||
|
|
|
@ -107,4 +107,10 @@
|
||||||
<dropTable tableName="USER_SESSION"/>
|
<dropTable tableName="USER_SESSION"/>
|
||||||
</changeSet>
|
</changeSet>
|
||||||
|
|
||||||
|
<changeSet author="keycloak" id="26.0.0-33201-org-redirect-url">
|
||||||
|
<addColumn tableName="ORG">
|
||||||
|
<column name="REDIRECT_URL" type="VARCHAR(2048)"/>
|
||||||
|
</addColumn>
|
||||||
|
</changeSet>
|
||||||
|
|
||||||
</databaseChangeLog>
|
</databaseChangeLog>
|
||||||
|
|
|
@ -31,10 +31,8 @@ import org.keycloak.exportimport.ExportOptions;
|
||||||
import org.keycloak.models.ClientModel;
|
import org.keycloak.models.ClientModel;
|
||||||
import org.keycloak.models.ClientScopeModel;
|
import org.keycloak.models.ClientScopeModel;
|
||||||
import org.keycloak.models.FederatedIdentityModel;
|
import org.keycloak.models.FederatedIdentityModel;
|
||||||
import org.keycloak.models.GroupModel;
|
|
||||||
import org.keycloak.models.GroupModel.Type;
|
import org.keycloak.models.GroupModel.Type;
|
||||||
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.RoleContainerModel;
|
import org.keycloak.models.RoleContainerModel;
|
||||||
import org.keycloak.models.RoleModel;
|
import org.keycloak.models.RoleModel;
|
||||||
|
@ -46,7 +44,6 @@ import org.keycloak.representations.idm.ComponentExportRepresentation;
|
||||||
import org.keycloak.representations.idm.CredentialRepresentation;
|
import org.keycloak.representations.idm.CredentialRepresentation;
|
||||||
import org.keycloak.representations.idm.FederatedIdentityRepresentation;
|
import org.keycloak.representations.idm.FederatedIdentityRepresentation;
|
||||||
import org.keycloak.representations.idm.IdentityProviderRepresentation;
|
import org.keycloak.representations.idm.IdentityProviderRepresentation;
|
||||||
import org.keycloak.representations.idm.OrganizationDomainRepresentation;
|
|
||||||
import org.keycloak.representations.idm.OrganizationRepresentation;
|
import org.keycloak.representations.idm.OrganizationRepresentation;
|
||||||
import org.keycloak.representations.idm.RealmRepresentation;
|
import org.keycloak.representations.idm.RealmRepresentation;
|
||||||
import org.keycloak.representations.idm.RoleRepresentation;
|
import org.keycloak.representations.idm.RoleRepresentation;
|
||||||
|
|
|
@ -66,6 +66,7 @@ import org.keycloak.models.utils.KeycloakModelUtils;
|
||||||
import org.keycloak.models.utils.ModelToRepresentation;
|
import org.keycloak.models.utils.ModelToRepresentation;
|
||||||
import org.keycloak.models.utils.RepresentationToModel;
|
import org.keycloak.models.utils.RepresentationToModel;
|
||||||
import org.keycloak.organization.OrganizationProvider;
|
import org.keycloak.organization.OrganizationProvider;
|
||||||
|
import org.keycloak.organization.validation.OrganizationsValidation;
|
||||||
import org.keycloak.partialimport.PartialImportResults;
|
import org.keycloak.partialimport.PartialImportResults;
|
||||||
import org.keycloak.protocol.oidc.OIDCConfigAttributes;
|
import org.keycloak.protocol.oidc.OIDCConfigAttributes;
|
||||||
import org.keycloak.representations.idm.ApplicationRepresentation;
|
import org.keycloak.representations.idm.ApplicationRepresentation;
|
||||||
|
@ -83,6 +84,8 @@ import org.keycloak.representations.idm.FederatedIdentityRepresentation;
|
||||||
import org.keycloak.representations.idm.GroupRepresentation;
|
import org.keycloak.representations.idm.GroupRepresentation;
|
||||||
import org.keycloak.representations.idm.IdentityProviderMapperRepresentation;
|
import org.keycloak.representations.idm.IdentityProviderMapperRepresentation;
|
||||||
import org.keycloak.representations.idm.IdentityProviderRepresentation;
|
import org.keycloak.representations.idm.IdentityProviderRepresentation;
|
||||||
|
import org.keycloak.representations.idm.MemberRepresentation;
|
||||||
|
import org.keycloak.representations.idm.MembershipType;
|
||||||
import org.keycloak.representations.idm.OAuthClientRepresentation;
|
import org.keycloak.representations.idm.OAuthClientRepresentation;
|
||||||
import org.keycloak.representations.idm.OrganizationRepresentation;
|
import org.keycloak.representations.idm.OrganizationRepresentation;
|
||||||
import org.keycloak.representations.idm.PartialImportRepresentation;
|
import org.keycloak.representations.idm.PartialImportRepresentation;
|
||||||
|
@ -132,8 +135,6 @@ import static org.keycloak.models.utils.RepresentationToModel.createRoleMappings
|
||||||
import static org.keycloak.models.utils.RepresentationToModel.importGroup;
|
import static org.keycloak.models.utils.RepresentationToModel.importGroup;
|
||||||
import static org.keycloak.models.utils.RepresentationToModel.importRoles;
|
import static org.keycloak.models.utils.RepresentationToModel.importRoles;
|
||||||
import static org.keycloak.models.utils.StripSecretsUtils.stripSecrets;
|
import static org.keycloak.models.utils.StripSecretsUtils.stripSecrets;
|
||||||
import org.keycloak.representations.idm.MemberRepresentation;
|
|
||||||
import org.keycloak.representations.idm.MembershipType;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This wraps the functionality about export/import for the storage.
|
* This wraps the functionality about export/import for the storage.
|
||||||
|
@ -1589,6 +1590,7 @@ 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())) {
|
||||||
|
OrganizationsValidation.validateUrl(orgRep.getRedirectUrl());
|
||||||
OrganizationModel orgModel = provider.create(orgRep.getId(), orgRep.getName(), orgRep.getAlias());
|
OrganizationModel orgModel = provider.create(orgRep.getId(), orgRep.getName(), orgRep.getAlias());
|
||||||
RepresentationToModel.toModel(orgRep, orgModel);
|
RepresentationToModel.toModel(orgRep, orgModel);
|
||||||
|
|
||||||
|
|
|
@ -1310,6 +1310,7 @@ public class ModelToRepresentation {
|
||||||
rep.setName(model.getName());
|
rep.setName(model.getName());
|
||||||
rep.setAlias(model.getAlias());
|
rep.setAlias(model.getAlias());
|
||||||
rep.setEnabled(model.isEnabled());
|
rep.setEnabled(model.isEnabled());
|
||||||
|
rep.setRedirectUrl(model.getRedirectUrl());
|
||||||
rep.setDescription(model.getDescription());
|
rep.setDescription(model.getDescription());
|
||||||
model.getDomains().filter(Objects::nonNull).map(ModelToRepresentation::toRepresentation)
|
model.getDomains().filter(Objects::nonNull).map(ModelToRepresentation::toRepresentation)
|
||||||
.forEach(rep::addDomain);
|
.forEach(rep::addDomain);
|
||||||
|
|
|
@ -1686,6 +1686,7 @@ public class RepresentationToModel {
|
||||||
model.setName(rep.getName());
|
model.setName(rep.getName());
|
||||||
model.setAlias(rep.getAlias());
|
model.setAlias(rep.getAlias());
|
||||||
model.setEnabled(rep.isEnabled());
|
model.setEnabled(rep.isEnabled());
|
||||||
|
model.setRedirectUrl(rep.getRedirectUrl());
|
||||||
model.setDescription(rep.getDescription());
|
model.setDescription(rep.getDescription());
|
||||||
model.setAttributes(rep.getAttributes());
|
model.setAttributes(rep.getAttributes());
|
||||||
model.setDomains(ofNullable(rep.getDomains()).orElse(Set.of()).stream()
|
model.setDomains(ofNullable(rep.getDomains()).orElse(Set.of()).stream()
|
||||||
|
|
|
@ -0,0 +1,33 @@
|
||||||
|
/*
|
||||||
|
* 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.organization.validation;
|
||||||
|
|
||||||
|
import org.keycloak.validate.BuiltinValidators;
|
||||||
|
|
||||||
|
public class OrganizationsValidation {
|
||||||
|
public static void validateUrl(String redirectUrl) {
|
||||||
|
if (!BuiltinValidators.uriValidator().validate(redirectUrl).isValid()) {
|
||||||
|
throw new OrganizationValidationException("Organization redirect URL is not valid.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class OrganizationValidationException extends RuntimeException {
|
||||||
|
public OrganizationValidationException(String message) {
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -54,9 +54,9 @@ public class UriValidator extends AbstractSimpleValidator implements ConfiguredP
|
||||||
public static final String MESSAGE_INVALID_SCHEME = "error-invalid-uri-scheme";
|
public static final String MESSAGE_INVALID_SCHEME = "error-invalid-uri-scheme";
|
||||||
public static final String MESSAGE_INVALID_FRAGMENT = "error-invalid-uri-fragment";
|
public static final String MESSAGE_INVALID_FRAGMENT = "error-invalid-uri-fragment";
|
||||||
|
|
||||||
public static boolean DEFAULT_ALLOW_FRAGMENT = true;
|
public static final boolean DEFAULT_ALLOW_FRAGMENT = true;
|
||||||
|
|
||||||
public static boolean DEFAULT_REQUIRE_VALID_URL = true;
|
public static final boolean DEFAULT_REQUIRE_VALID_URL = true;
|
||||||
|
|
||||||
public static final String ID = "uri";
|
public static final String ID = "uri";
|
||||||
|
|
||||||
|
@ -102,8 +102,8 @@ public class UriValidator extends AbstractSimpleValidator implements ConfiguredP
|
||||||
@Override
|
@Override
|
||||||
protected void doValidate(Object input, String inputHint, ValidationContext context, ValidatorConfig config) {
|
protected void doValidate(Object input, String inputHint, ValidationContext context, ValidatorConfig config) {
|
||||||
|
|
||||||
if(input == null || (input instanceof String && ((String) input).isEmpty())) {
|
if (input == null || (input instanceof String && ((String) input).isEmpty())) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
@ -125,13 +125,12 @@ public class UriValidator extends AbstractSimpleValidator implements ConfiguredP
|
||||||
|
|
||||||
private URI toUri(Object input) throws URISyntaxException {
|
private URI toUri(Object input) throws URISyntaxException {
|
||||||
|
|
||||||
if (input instanceof String) {
|
if (input instanceof String uriString) {
|
||||||
String uriString = (String) input;
|
|
||||||
return new URI(uriString);
|
return new URI(uriString);
|
||||||
} else if (input instanceof URI) {
|
} else if (input instanceof URI uri) {
|
||||||
return (URI) input;
|
return uri;
|
||||||
} else if (input instanceof URL) {
|
} else if (input instanceof URL url) {
|
||||||
return ((URL) input).toURI();
|
return url.toURI();
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
|
|
|
@ -65,6 +65,10 @@ public interface OrganizationModel {
|
||||||
|
|
||||||
void setDescription(String description);
|
void setDescription(String description);
|
||||||
|
|
||||||
|
String getRedirectUrl();
|
||||||
|
|
||||||
|
void setRedirectUrl(String redirectUrl);
|
||||||
|
|
||||||
Map<String, List<String>> getAttributes();
|
Map<String, List<String>> getAttributes();
|
||||||
|
|
||||||
void setAttributes(Map<String, List<String>> attributes);
|
void setAttributes(Map<String, List<String>> attributes);
|
||||||
|
|
|
@ -143,7 +143,7 @@ public class InviteOrgActionTokenHandler extends AbstractActionTokenHandler<Invi
|
||||||
// if we made it this far then go ahead and add the user to the organization
|
// if we made it this far then go ahead and add the user to the organization
|
||||||
orgProvider.addMember(orgProvider.getById(token.getOrgId()), user);
|
orgProvider.addMember(orgProvider.getById(token.getOrgId()), user);
|
||||||
|
|
||||||
String redirectUri = RedirectUtils.verifyRedirectUri(tokenContext.getSession(), token.getRedirectUri(), authSession.getClient());
|
String redirectUri = token.getRedirectUri();
|
||||||
|
|
||||||
if (redirectUri != null) {
|
if (redirectUri != null) {
|
||||||
authSession.setAuthNote(AuthenticationManager.SET_REDIRECT_URI_AFTER_REQUIRED_ACTIONS, "true");
|
authSession.setAuthNote(AuthenticationManager.SET_REDIRECT_URI_AFTER_REQUIRED_ACTIONS, "true");
|
||||||
|
|
|
@ -145,8 +145,11 @@ public class OrganizationInvitationResource {
|
||||||
|
|
||||||
token.setOrgId(organization.getId());
|
token.setOrgId(organization.getId());
|
||||||
|
|
||||||
String redirectUri = Urls.accountBase(session.getContext().getUri().getBaseUri()).path("/").build(realm.getName()).toString();
|
if (organization.getRedirectUrl() == null || organization.getRedirectUrl().isBlank()) {
|
||||||
token.setRedirectUri(redirectUri);
|
token.setRedirectUri(Urls.accountBase(session.getContext().getUri().getBaseUri()).path("/").build(realm.getName()).toString());
|
||||||
|
} else {
|
||||||
|
token.setRedirectUri(organization.getRedirectUrl());
|
||||||
|
}
|
||||||
|
|
||||||
return token.serialize(session, realm, session.getContext().getUri());
|
return token.serialize(session, realm, session.getContext().getUri());
|
||||||
}
|
}
|
||||||
|
|
|
@ -39,6 +39,8 @@ import org.keycloak.models.OrganizationModel;
|
||||||
import org.keycloak.models.utils.ModelToRepresentation;
|
import org.keycloak.models.utils.ModelToRepresentation;
|
||||||
import org.keycloak.models.utils.RepresentationToModel;
|
import org.keycloak.models.utils.RepresentationToModel;
|
||||||
import org.keycloak.organization.OrganizationProvider;
|
import org.keycloak.organization.OrganizationProvider;
|
||||||
|
import org.keycloak.organization.validation.OrganizationsValidation;
|
||||||
|
import org.keycloak.organization.validation.OrganizationsValidation.OrganizationValidationException;
|
||||||
import org.keycloak.representations.idm.OrganizationRepresentation;
|
import org.keycloak.representations.idm.OrganizationRepresentation;
|
||||||
import org.keycloak.services.ErrorResponse;
|
import org.keycloak.services.ErrorResponse;
|
||||||
import org.keycloak.services.resources.KeycloakOpenAPI;
|
import org.keycloak.services.resources.KeycloakOpenAPI;
|
||||||
|
@ -93,11 +95,12 @@ public class OrganizationResource {
|
||||||
@Operation(summary = "Updates the organization")
|
@Operation(summary = "Updates the organization")
|
||||||
public Response update(OrganizationRepresentation organizationRep) {
|
public Response update(OrganizationRepresentation organizationRep) {
|
||||||
try {
|
try {
|
||||||
|
OrganizationsValidation.validateUrl(organizationRep.getRedirectUrl());
|
||||||
RepresentationToModel.toModel(organizationRep, organization);
|
RepresentationToModel.toModel(organizationRep, organization);
|
||||||
adminEvent.operation(OperationType.UPDATE).resourcePath(session.getContext().getUri()).representation(organizationRep).success();
|
adminEvent.operation(OperationType.UPDATE).resourcePath(session.getContext().getUri()).representation(organizationRep).success();
|
||||||
return Response.noContent().build();
|
return Response.noContent().build();
|
||||||
} catch (ModelValidationException mve) {
|
} catch (ModelValidationException | OrganizationValidationException ex) {
|
||||||
throw ErrorResponse.error(mve.getMessage(), Response.Status.BAD_REQUEST);
|
throw ErrorResponse.error(ex.getMessage(), Response.Status.BAD_REQUEST);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -49,6 +49,8 @@ import org.keycloak.models.utils.ModelToRepresentation;
|
||||||
import org.keycloak.models.utils.RepresentationToModel;
|
import org.keycloak.models.utils.RepresentationToModel;
|
||||||
import org.keycloak.organization.OrganizationProvider;
|
import org.keycloak.organization.OrganizationProvider;
|
||||||
import org.keycloak.organization.utils.Organizations;
|
import org.keycloak.organization.utils.Organizations;
|
||||||
|
import org.keycloak.organization.validation.OrganizationsValidation;
|
||||||
|
import org.keycloak.organization.validation.OrganizationsValidation.OrganizationValidationException;
|
||||||
import org.keycloak.representations.idm.OrganizationRepresentation;
|
import org.keycloak.representations.idm.OrganizationRepresentation;
|
||||||
import org.keycloak.services.ErrorResponse;
|
import org.keycloak.services.ErrorResponse;
|
||||||
import org.keycloak.services.resources.KeycloakOpenAPI;
|
import org.keycloak.services.resources.KeycloakOpenAPI;
|
||||||
|
@ -102,15 +104,17 @@ public class OrganizationsResource {
|
||||||
ReservedCharValidator.validateNoSpace(organization.getAlias());
|
ReservedCharValidator.validateNoSpace(organization.getAlias());
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
OrganizationsValidation.validateUrl(organization.getRedirectUrl());
|
||||||
|
|
||||||
OrganizationModel model = provider.create(organization.getName(), organization.getAlias());
|
OrganizationModel model = provider.create(organization.getName(), organization.getAlias());
|
||||||
RepresentationToModel.toModel(organization, model);
|
RepresentationToModel.toModel(organization, model);
|
||||||
organization.setId(model.getId());
|
organization.setId(model.getId());
|
||||||
adminEvent.operation(OperationType.CREATE).resourcePath(session.getContext().getUri(), model.getId()).representation(organization).success();
|
adminEvent.operation(OperationType.CREATE).resourcePath(session.getContext().getUri(), model.getId()).representation(organization).success();
|
||||||
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 | OrganizationValidationException ex) {
|
||||||
throw ErrorResponse.error(mve.getMessage(), Response.Status.BAD_REQUEST);
|
throw ErrorResponse.error(ex.getMessage(), Response.Status.BAD_REQUEST);
|
||||||
} catch (ModelDuplicateException mve) {
|
} catch (ModelDuplicateException mde) {
|
||||||
throw ErrorResponse.error(mve.getMessage(), Status.CONFLICT);
|
throw ErrorResponse.error(mde.getMessage(), Status.CONFLICT);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -17,6 +17,7 @@
|
||||||
|
|
||||||
package org.keycloak.testsuite.pages;
|
package org.keycloak.testsuite.pages;
|
||||||
|
|
||||||
|
import org.keycloak.testsuite.util.OAuthClient;
|
||||||
import org.openqa.selenium.WebElement;
|
import org.openqa.selenium.WebElement;
|
||||||
import org.openqa.selenium.support.FindBy;
|
import org.openqa.selenium.support.FindBy;
|
||||||
|
|
||||||
|
@ -33,12 +34,12 @@ public class AppPage extends AbstractPage {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void open() {
|
public void open() {
|
||||||
driver.navigate().to(oauth.APP_AUTH_ROOT);
|
driver.navigate().to(OAuthClient.APP_AUTH_ROOT);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean isCurrent() {
|
public boolean isCurrent() {
|
||||||
return removeDefaultPorts(driver.getCurrentUrl()).startsWith(oauth.APP_AUTH_ROOT);
|
return removeDefaultPorts(driver.getCurrentUrl()).startsWith(OAuthClient.APP_AUTH_ROOT);
|
||||||
}
|
}
|
||||||
|
|
||||||
public RequestType getRequestType() {
|
public RequestType getRequestType() {
|
||||||
|
|
|
@ -0,0 +1,32 @@
|
||||||
|
/*
|
||||||
|
* 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.updaters;
|
||||||
|
|
||||||
|
import org.keycloak.admin.client.resource.OrganizationResource;
|
||||||
|
import org.keycloak.representations.idm.OrganizationRepresentation;
|
||||||
|
|
||||||
|
public class OrganizationAttributeUpdater extends ServerResourceUpdater<OrganizationAttributeUpdater, OrganizationResource, OrganizationRepresentation> {
|
||||||
|
|
||||||
|
public OrganizationAttributeUpdater(OrganizationResource resource) {
|
||||||
|
super(resource, resource::toRepresentation, resource::update);
|
||||||
|
}
|
||||||
|
|
||||||
|
public OrganizationAttributeUpdater setRedirectUrl(String redirectUrl) {
|
||||||
|
this.rep.setRedirectUrl(redirectUrl);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
}
|
|
@ -35,6 +35,7 @@ import jakarta.ws.rs.core.Response;
|
||||||
import java.time.Duration;
|
import java.time.Duration;
|
||||||
import org.hamcrest.Matchers;
|
import org.hamcrest.Matchers;
|
||||||
import org.jboss.arquillian.graphene.page.Page;
|
import org.jboss.arquillian.graphene.page.Page;
|
||||||
|
import org.junit.Before;
|
||||||
import org.junit.Rule;
|
import org.junit.Rule;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
import org.keycloak.admin.client.resource.OrganizationResource;
|
import org.keycloak.admin.client.resource.OrganizationResource;
|
||||||
|
@ -49,10 +50,12 @@ import org.keycloak.testsuite.AssertEvents;
|
||||||
import org.keycloak.testsuite.admin.ApiUtil;
|
import org.keycloak.testsuite.admin.ApiUtil;
|
||||||
import org.keycloak.testsuite.pages.InfoPage;
|
import org.keycloak.testsuite.pages.InfoPage;
|
||||||
import org.keycloak.testsuite.pages.RegisterPage;
|
import org.keycloak.testsuite.pages.RegisterPage;
|
||||||
|
import org.keycloak.testsuite.updaters.OrganizationAttributeUpdater;
|
||||||
import org.keycloak.testsuite.updaters.RealmAttributeUpdater;
|
import org.keycloak.testsuite.updaters.RealmAttributeUpdater;
|
||||||
import org.keycloak.testsuite.util.GreenMailRule;
|
import org.keycloak.testsuite.util.GreenMailRule;
|
||||||
import org.keycloak.testsuite.util.MailUtils;
|
import org.keycloak.testsuite.util.MailUtils;
|
||||||
import org.keycloak.testsuite.util.MailUtils.EmailBody;
|
import org.keycloak.testsuite.util.MailUtils.EmailBody;
|
||||||
|
import org.keycloak.testsuite.util.OAuthClient;
|
||||||
import org.keycloak.testsuite.util.UserBuilder;
|
import org.keycloak.testsuite.util.UserBuilder;
|
||||||
|
|
||||||
public class OrganizationInvitationLinkTest extends AbstractOrganizationTest {
|
public class OrganizationInvitationLinkTest extends AbstractOrganizationTest {
|
||||||
|
@ -69,6 +72,11 @@ public class OrganizationInvitationLinkTest extends AbstractOrganizationTest {
|
||||||
@Page
|
@Page
|
||||||
protected RegisterPage registerPage;
|
protected RegisterPage registerPage;
|
||||||
|
|
||||||
|
@Before
|
||||||
|
public void setDriverTimeout() {
|
||||||
|
driver.manage().timeouts().pageLoadTimeout(Duration.ofMinutes(1));
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void configureTestRealm(RealmRepresentation testRealm) {
|
public void configureTestRealm(RealmRepresentation testRealm) {
|
||||||
Map<String, String> smtpConfig = testRealm.getSmtpServer();
|
Map<String, String> smtpConfig = testRealm.getSmtpServer();
|
||||||
|
@ -87,6 +95,22 @@ public class OrganizationInvitationLinkTest extends AbstractOrganizationTest {
|
||||||
acceptInvitation(organization, user);
|
acceptInvitation(organization, user);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testInviteExistingUserCustomRedirectUrl() throws IOException, MessagingException {
|
||||||
|
UserRepresentation user = createUser("invited", "invited@myemail.com");
|
||||||
|
|
||||||
|
OrganizationResource organization = testRealm().organizations().get(createOrganization().getId());
|
||||||
|
|
||||||
|
try (
|
||||||
|
OrganizationAttributeUpdater oau = new OrganizationAttributeUpdater(organization).setRedirectUrl(OAuthClient.APP_AUTH_ROOT).update();
|
||||||
|
Response response = organization.members().inviteExistingUser(user.getId());
|
||||||
|
) {
|
||||||
|
assertThat(response.getStatus(), equalTo(Response.Status.NO_CONTENT.getStatusCode()));
|
||||||
|
|
||||||
|
acceptInvitation(organization, user, "AUTH_RESPONSE");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testInviteExistingUserWithEmail() throws IOException, MessagingException {
|
public void testInviteExistingUserWithEmail() throws IOException, MessagingException {
|
||||||
UserRepresentation user = createUser("invitedWithMatchingEmail", "invitedWithMatchingEmail@myemail.com");
|
UserRepresentation user = createUser("invitedWithMatchingEmail", "invitedWithMatchingEmail@myemail.com");
|
||||||
|
@ -98,6 +122,22 @@ public class OrganizationInvitationLinkTest extends AbstractOrganizationTest {
|
||||||
acceptInvitation(organization, user);
|
acceptInvitation(organization, user);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testInviteExistingUserWithEmailCustomRedirectUrl() throws IOException, MessagingException {
|
||||||
|
UserRepresentation user = createUser("invitedWithMatchingEmail", "invitedWithMatchingEmail@myemail.com");
|
||||||
|
|
||||||
|
OrganizationResource organization = testRealm().organizations().get(createOrganization().getId());
|
||||||
|
|
||||||
|
try (
|
||||||
|
OrganizationAttributeUpdater oau = new OrganizationAttributeUpdater(organization).setRedirectUrl(OAuthClient.APP_AUTH_ROOT).update();
|
||||||
|
Response response = organization.members().inviteUser(user.getEmail(), "Homer", "Simpson");
|
||||||
|
) {
|
||||||
|
assertThat(response.getStatus(), equalTo(Response.Status.NO_CONTENT.getStatusCode()));
|
||||||
|
|
||||||
|
acceptInvitation(organization, user, "AUTH_RESPONSE");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testInviteNewUserRegistration() throws IOException, MessagingException {
|
public void testInviteNewUserRegistration() throws IOException, MessagingException {
|
||||||
String email = "inviteduser@email";
|
String email = "inviteduser@email";
|
||||||
|
@ -122,6 +162,34 @@ public class OrganizationInvitationLinkTest extends AbstractOrganizationTest {
|
||||||
Assert.assertNotNull(driver.manage().getCookieNamed(CookieType.IDENTITY.getName()));
|
Assert.assertNotNull(driver.manage().getCookieNamed(CookieType.IDENTITY.getName()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testInviteNewUserRegistrationCustomRedirectUrl() throws IOException, MessagingException {
|
||||||
|
String email = "inviteduser@email";
|
||||||
|
String firstName = "Homer";
|
||||||
|
String lastName = "Simpson";
|
||||||
|
|
||||||
|
OrganizationResource organization = testRealm().organizations().get(createOrganization().getId());
|
||||||
|
try (
|
||||||
|
OrganizationAttributeUpdater oau = new OrganizationAttributeUpdater(organization).setRedirectUrl(OAuthClient.APP_AUTH_ROOT).update();
|
||||||
|
Response response = organization.members().inviteUser(email, firstName, lastName);
|
||||||
|
) {
|
||||||
|
assertThat(response.getStatus(), equalTo(Response.Status.NO_CONTENT.getStatusCode()));
|
||||||
|
|
||||||
|
registerUser(organization, email);
|
||||||
|
|
||||||
|
List<UserRepresentation> users = testRealm().users().searchByEmail(email, true);
|
||||||
|
assertThat(users, Matchers.not(empty()));
|
||||||
|
// user is a member
|
||||||
|
MemberRepresentation member = organization.members().member(users.get(0).getId()).toRepresentation();
|
||||||
|
Assert.assertNotNull(member);
|
||||||
|
assertThat(member.getMembershipType(), equalTo(MembershipType.MANAGED));
|
||||||
|
getCleanup().addCleanup(() -> testRealm().users().get(users.get(0).getId()).remove());
|
||||||
|
|
||||||
|
// authenticated to the app
|
||||||
|
assertThat(driver.getTitle(), containsString("AUTH_RESPONSE"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testRegistrationEnabledWhenInvitingNewUser() throws Exception {
|
public void testRegistrationEnabledWhenInvitingNewUser() throws Exception {
|
||||||
String email = "inviteduser@email";
|
String email = "inviteduser@email";
|
||||||
|
@ -168,6 +236,7 @@ public class OrganizationInvitationLinkTest extends AbstractOrganizationTest {
|
||||||
try (Response response = testRealm().users().create(user)) {
|
try (Response response = testRealm().users().create(user)) {
|
||||||
user.setId(ApiUtil.getCreatedId(response));
|
user.setId(ApiUtil.getCreatedId(response));
|
||||||
}
|
}
|
||||||
|
getCleanup().addUserId(user.getId());
|
||||||
return user;
|
return user;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -224,13 +293,16 @@ public class OrganizationInvitationLinkTest extends AbstractOrganizationTest {
|
||||||
driver.navigate().to(link);
|
driver.navigate().to(link);
|
||||||
Assert.assertFalse(organization.members().getAll().stream().anyMatch(actual -> email.equals(actual.getEmail())));
|
Assert.assertFalse(organization.members().getAll().stream().anyMatch(actual -> email.equals(actual.getEmail())));
|
||||||
registerPage.assertCurrent(organizationName);
|
registerPage.assertCurrent(organizationName);
|
||||||
driver.manage().timeouts().pageLoadTimeout(Duration.ofSeconds(10));
|
|
||||||
assertThat(registerPage.getEmail(), equalTo(expectedEmail));
|
assertThat(registerPage.getEmail(), equalTo(expectedEmail));
|
||||||
registerPage.register("firstName", "lastName", email,
|
registerPage.register("firstName", "lastName", email,
|
||||||
"invitedUser", "password", "password", null, false, null);
|
"invitedUser", "password", "password", null, false, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void acceptInvitation(OrganizationResource organization, UserRepresentation user) throws MessagingException, IOException {
|
private void acceptInvitation(OrganizationResource organization, UserRepresentation user) throws MessagingException, IOException {
|
||||||
|
acceptInvitation(organization, user, "Account Management");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void acceptInvitation(OrganizationResource organization, UserRepresentation user, String pageTitle) throws MessagingException, IOException {
|
||||||
String link = getInvitationLinkFromEmail(user.getFirstName(), user.getLastName());
|
String link = getInvitationLinkFromEmail(user.getFirstName(), user.getLastName());
|
||||||
driver.navigate().to(link);
|
driver.navigate().to(link);
|
||||||
// not yet a member
|
// not yet a member
|
||||||
|
@ -239,8 +311,8 @@ public class OrganizationInvitationLinkTest extends AbstractOrganizationTest {
|
||||||
assertThat(driver.getPageSource(), containsString("You are about to join organization " + organizationName));
|
assertThat(driver.getPageSource(), containsString("You are about to join organization " + organizationName));
|
||||||
assertThat(infoPage.getInfo(), containsString("By clicking on the link below, you will become a member of the " + organizationName + " organization:"));
|
assertThat(infoPage.getInfo(), containsString("By clicking on the link below, you will become a member of the " + organizationName + " organization:"));
|
||||||
infoPage.clickToContinue();
|
infoPage.clickToContinue();
|
||||||
// redirect to the account console and eventually force the user to authenticate if not already
|
// redirect to the redirectUrl and eventually force the user to authenticate if not already
|
||||||
assertThat(driver.getTitle(), containsString("Account Management"));
|
assertThat(driver.getTitle(), containsString(pageTitle));
|
||||||
// now a member
|
// now a member
|
||||||
Assert.assertNotNull(organization.members().member(user.getId()).toRepresentation());
|
Assert.assertNotNull(organization.members().member(user.getId()).toRepresentation());
|
||||||
}
|
}
|
||||||
|
|
|
@ -551,4 +551,41 @@ public class OrganizationTest extends AbstractOrganizationTest {
|
||||||
assertEquals(Status.BAD_REQUEST.getStatusCode(), response.getStatus());
|
assertEquals(Status.BAD_REQUEST.getStatusCode(), response.getStatus());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testInvalidRedirectUri() {
|
||||||
|
OrganizationRepresentation expected = createOrganization();
|
||||||
|
expected.setRedirectUrl("http://valid.url:8080/");
|
||||||
|
|
||||||
|
OrganizationResource organization = testRealm().organizations().get(expected.getId());
|
||||||
|
|
||||||
|
try (Response response = organization.update(expected)) {
|
||||||
|
assertThat(response.getStatus(), equalTo(Status.NO_CONTENT.getStatusCode()));
|
||||||
|
assertThat(organization.toRepresentation().getRedirectUrl(), equalTo("http://valid.url:8080/"));
|
||||||
|
}
|
||||||
|
|
||||||
|
expected.setRedirectUrl("");
|
||||||
|
try (Response response = organization.update(expected)) {
|
||||||
|
assertThat(response.getStatus(), equalTo(Status.NO_CONTENT.getStatusCode()));
|
||||||
|
assertThat(organization.toRepresentation().getRedirectUrl(), nullValue());
|
||||||
|
}
|
||||||
|
|
||||||
|
expected.setRedirectUrl(" ");
|
||||||
|
try (Response response = organization.update(expected)) {
|
||||||
|
assertThat(response.getStatus(), equalTo(Status.BAD_REQUEST.getStatusCode()));
|
||||||
|
assertThat(organization.toRepresentation().getRedirectUrl(), nullValue());
|
||||||
|
}
|
||||||
|
|
||||||
|
expected.setRedirectUrl("invalid");
|
||||||
|
try (Response response = organization.update(expected)) {
|
||||||
|
assertThat(response.getStatus(), equalTo(Status.BAD_REQUEST.getStatusCode()));
|
||||||
|
assertThat(organization.toRepresentation().getRedirectUrl(), nullValue());
|
||||||
|
}
|
||||||
|
|
||||||
|
expected.setRedirectUrl("https://\ninvalid");
|
||||||
|
try (Response response = organization.update(expected)) {
|
||||||
|
assertThat(response.getStatus(), equalTo(Status.BAD_REQUEST.getStatusCode()));
|
||||||
|
assertThat(organization.toRepresentation().getRedirectUrl(), nullValue());
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,6 +22,7 @@ import static org.hamcrest.CoreMatchers.is;
|
||||||
import static org.hamcrest.CoreMatchers.notNullValue;
|
import static org.hamcrest.CoreMatchers.notNullValue;
|
||||||
import static org.hamcrest.MatcherAssert.assertThat;
|
import static org.hamcrest.MatcherAssert.assertThat;
|
||||||
import static org.hamcrest.Matchers.containsInAnyOrder;
|
import static org.hamcrest.Matchers.containsInAnyOrder;
|
||||||
|
import static org.hamcrest.Matchers.equalTo;
|
||||||
import static org.hamcrest.Matchers.hasSize;
|
import static org.hamcrest.Matchers.hasSize;
|
||||||
import static org.junit.Assert.assertEquals;
|
import static org.junit.Assert.assertEquals;
|
||||||
import static org.junit.Assert.assertTrue;
|
import static org.junit.Assert.assertTrue;
|
||||||
|
@ -46,7 +47,6 @@ import org.keycloak.exportimport.singlefile.SingleFileExportProviderFactory;
|
||||||
import org.keycloak.exportimport.singlefile.SingleFileImportProviderFactory;
|
import org.keycloak.exportimport.singlefile.SingleFileImportProviderFactory;
|
||||||
import org.keycloak.models.OrganizationModel;
|
import org.keycloak.models.OrganizationModel;
|
||||||
import org.keycloak.models.utils.DefaultAuthenticationFlows;
|
import org.keycloak.models.utils.DefaultAuthenticationFlows;
|
||||||
import org.keycloak.organization.OrganizationProvider;
|
|
||||||
import org.keycloak.representations.idm.AuthenticationExecutionInfoRepresentation;
|
import org.keycloak.representations.idm.AuthenticationExecutionInfoRepresentation;
|
||||||
import org.keycloak.representations.idm.IdentityProviderRepresentation;
|
import org.keycloak.representations.idm.IdentityProviderRepresentation;
|
||||||
import org.keycloak.representations.idm.OrganizationRepresentation;
|
import org.keycloak.representations.idm.OrganizationRepresentation;
|
||||||
|
@ -75,6 +75,11 @@ public class OrganizationExportTest extends AbstractOrganizationTest {
|
||||||
OrganizationRepresentation orgRep = createOrganization(testRealm(), getCleanup(), "org-" + i, broker, domain);
|
OrganizationRepresentation orgRep = createOrganization(testRealm(), getCleanup(), "org-" + i, broker, domain);
|
||||||
OrganizationResource organization = testRealm().organizations().get(orgRep.getId());
|
OrganizationResource organization = testRealm().organizations().get(orgRep.getId());
|
||||||
|
|
||||||
|
orgRep.setRedirectUrl("https://0.0.0.0:8080");
|
||||||
|
try (Response response = organization.update(orgRep)) {
|
||||||
|
assertThat(response.getStatus(), equalTo(Response.Status.NO_CONTENT.getStatusCode()));
|
||||||
|
}
|
||||||
|
|
||||||
expectedOrganizations.add(orgRep);
|
expectedOrganizations.add(orgRep);
|
||||||
|
|
||||||
for (int j = 0; j < 3; j++) {
|
for (int j = 0; j < 3; j++) {
|
||||||
|
@ -114,7 +119,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());
|
||||||
// id, name, alias, and description should have all been preserved.
|
// id, name, alias, description and redirectUrl should have all been preserved.
|
||||||
assertThat(organizations.stream().map(OrganizationRepresentation::getId).toList(),
|
assertThat(organizations.stream().map(OrganizationRepresentation::getId).toList(),
|
||||||
Matchers.containsInAnyOrder(expectedOrganizations.stream().map(OrganizationRepresentation::getId).toArray()));
|
Matchers.containsInAnyOrder(expectedOrganizations.stream().map(OrganizationRepresentation::getId).toArray()));
|
||||||
assertThat(organizations.stream().map(OrganizationRepresentation::getName).toList(),
|
assertThat(organizations.stream().map(OrganizationRepresentation::getName).toList(),
|
||||||
|
@ -123,6 +128,8 @@ public class OrganizationExportTest extends AbstractOrganizationTest {
|
||||||
Matchers.containsInAnyOrder(expectedOrganizations.stream().map(OrganizationRepresentation::getAlias).toArray()));
|
Matchers.containsInAnyOrder(expectedOrganizations.stream().map(OrganizationRepresentation::getAlias).toArray()));
|
||||||
assertThat(organizations.stream().map(OrganizationRepresentation::getDescription).toList(),
|
assertThat(organizations.stream().map(OrganizationRepresentation::getDescription).toList(),
|
||||||
Matchers.containsInAnyOrder(expectedOrganizations.stream().map(OrganizationRepresentation::getDescription).toArray()));
|
Matchers.containsInAnyOrder(expectedOrganizations.stream().map(OrganizationRepresentation::getDescription).toArray()));
|
||||||
|
assertThat(organizations.stream().map(OrganizationRepresentation::getRedirectUrl).toList(),
|
||||||
|
Matchers.containsInAnyOrder(expectedOrganizations.stream().map(OrganizationRepresentation::getRedirectUrl).toArray()));
|
||||||
|
|
||||||
// the endpoint search method returns brief representations of orgs - to get full rep we need to fetch by id.
|
// the endpoint search method returns brief representations of orgs - to get full rep we need to fetch by id.
|
||||||
for (OrganizationRepresentation organization : organizations) {
|
for (OrganizationRepresentation organization : organizations) {
|
||||||
|
|
Loading…
Reference in a new issue