commit
a58c985934
36 changed files with 247 additions and 61 deletions
|
@ -25,6 +25,7 @@ import java.util.Map;
|
|||
public class IdentityProviderRepresentation {
|
||||
|
||||
protected String alias;
|
||||
protected String displayName;
|
||||
protected String internalId;
|
||||
protected String providerId;
|
||||
protected boolean enabled = true;
|
||||
|
@ -176,4 +177,12 @@ public class IdentityProviderRepresentation {
|
|||
this.trustEmail = trustEmail;
|
||||
}
|
||||
|
||||
public String getDisplayName() {
|
||||
return displayName;
|
||||
}
|
||||
|
||||
public void setDisplayName(String displayName) {
|
||||
this.displayName = displayName;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -18,10 +18,9 @@
|
|||
package org.keycloak.models.jpa;
|
||||
|
||||
import org.jboss.logging.Logger;
|
||||
import org.keycloak.common.util.MultivaluedHashMap;
|
||||
import org.keycloak.common.util.StringPropertyReplacer;
|
||||
import org.keycloak.component.ComponentModel;
|
||||
import org.keycloak.common.enums.SslRequired;
|
||||
import org.keycloak.common.util.MultivaluedHashMap;
|
||||
import org.keycloak.component.ComponentModel;
|
||||
import org.keycloak.jose.jwk.JWKBuilder;
|
||||
import org.keycloak.models.AuthenticationExecutionModel;
|
||||
import org.keycloak.models.AuthenticationFlowModel;
|
||||
|
@ -43,12 +42,28 @@ import org.keycloak.models.RoleModel;
|
|||
import org.keycloak.models.UserFederationMapperModel;
|
||||
import org.keycloak.models.UserFederationProviderCreationEventImpl;
|
||||
import org.keycloak.models.UserFederationProviderModel;
|
||||
import org.keycloak.models.jpa.entities.*;
|
||||
import org.keycloak.models.jpa.entities.AuthenticationExecutionEntity;
|
||||
import org.keycloak.models.jpa.entities.AuthenticationFlowEntity;
|
||||
import org.keycloak.models.jpa.entities.AuthenticatorConfigEntity;
|
||||
import org.keycloak.models.jpa.entities.ClientEntity;
|
||||
import org.keycloak.models.jpa.entities.ClientTemplateEntity;
|
||||
import org.keycloak.models.jpa.entities.ComponentConfigEntity;
|
||||
import org.keycloak.models.jpa.entities.ComponentEntity;
|
||||
import org.keycloak.models.jpa.entities.GroupEntity;
|
||||
import org.keycloak.models.jpa.entities.IdentityProviderEntity;
|
||||
import org.keycloak.models.jpa.entities.IdentityProviderMapperEntity;
|
||||
import org.keycloak.models.jpa.entities.RealmAttributeEntity;
|
||||
import org.keycloak.models.jpa.entities.RealmAttributes;
|
||||
import org.keycloak.models.jpa.entities.RealmEntity;
|
||||
import org.keycloak.models.jpa.entities.RequiredActionProviderEntity;
|
||||
import org.keycloak.models.jpa.entities.RequiredCredentialEntity;
|
||||
import org.keycloak.models.jpa.entities.RoleEntity;
|
||||
import org.keycloak.models.jpa.entities.UserFederationMapperEntity;
|
||||
import org.keycloak.models.jpa.entities.UserFederationProviderEntity;
|
||||
import org.keycloak.models.utils.KeycloakModelUtils;
|
||||
|
||||
import javax.persistence.EntityManager;
|
||||
import javax.persistence.TypedQuery;
|
||||
|
||||
import java.security.Key;
|
||||
import java.security.PrivateKey;
|
||||
import java.security.PublicKey;
|
||||
|
@ -1255,9 +1270,10 @@ public class RealmAdapter implements RealmModel, JpaModel<RealmEntity> {
|
|||
|
||||
for (IdentityProviderEntity entity: entities) {
|
||||
IdentityProviderModel identityProviderModel = new IdentityProviderModel();
|
||||
|
||||
identityProviderModel.setProviderId(entity.getProviderId());
|
||||
identityProviderModel.setAlias(entity.getAlias());
|
||||
identityProviderModel.setDisplayName(entity.getDisplayName());
|
||||
|
||||
identityProviderModel.setInternalId(entity.getInternalId());
|
||||
Map<String, String> config = entity.getConfig();
|
||||
Map<String, String> copy = new HashMap<>();
|
||||
|
@ -1294,6 +1310,7 @@ public class RealmAdapter implements RealmModel, JpaModel<RealmEntity> {
|
|||
|
||||
entity.setInternalId(KeycloakModelUtils.generateId());
|
||||
entity.setAlias(identityProvider.getAlias());
|
||||
entity.setDisplayName(identityProvider.getDisplayName());
|
||||
entity.setProviderId(identityProvider.getProviderId());
|
||||
entity.setEnabled(identityProvider.isEnabled());
|
||||
entity.setStoreToken(identityProvider.isStoreToken());
|
||||
|
@ -1327,6 +1344,7 @@ public class RealmAdapter implements RealmModel, JpaModel<RealmEntity> {
|
|||
for (IdentityProviderEntity entity : this.realm.getIdentityProviders()) {
|
||||
if (entity.getInternalId().equals(identityProvider.getInternalId())) {
|
||||
entity.setAlias(identityProvider.getAlias());
|
||||
entity.setDisplayName(identityProvider.getDisplayName());
|
||||
entity.setEnabled(identityProvider.isEnabled());
|
||||
entity.setTrustEmail(identityProvider.isTrustEmail());
|
||||
entity.setAuthenticateByDefault(identityProvider.isAuthenticateByDefault());
|
||||
|
|
|
@ -59,6 +59,9 @@ public class IdentityProviderEntity {
|
|||
@Column(name="PROVIDER_ALIAS")
|
||||
private String alias;
|
||||
|
||||
@Column(name="PROVIDER_DISPLAY_NAME")
|
||||
private String displayName;
|
||||
|
||||
@Column(name="ENABLED")
|
||||
private boolean enabled;
|
||||
|
||||
|
@ -182,6 +185,14 @@ public class IdentityProviderEntity {
|
|||
this.trustEmail = trustEmail;
|
||||
}
|
||||
|
||||
public String getDisplayName() {
|
||||
return displayName;
|
||||
}
|
||||
|
||||
public void setDisplayName(String displayName) {
|
||||
this.displayName = displayName;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
|
|
|
@ -33,8 +33,9 @@
|
|||
|
||||
<dropColumn tableName="USER_ENTITY" columnName="TOTP" />
|
||||
|
||||
|
||||
|
||||
<addColumn tableName="IDENTITY_PROVIDER">
|
||||
<column name="PROVIDER_DISPLAY_NAME" type="VARCHAR(255)"></column>
|
||||
</addColumn>
|
||||
</changeSet>
|
||||
|
||||
|
||||
|
|
|
@ -21,7 +21,6 @@ import com.mongodb.DBObject;
|
|||
import com.mongodb.QueryBuilder;
|
||||
|
||||
import org.keycloak.common.util.MultivaluedHashMap;
|
||||
import org.keycloak.common.util.StringPropertyReplacer;
|
||||
import org.keycloak.component.ComponentModel;
|
||||
import org.keycloak.connections.mongo.api.context.MongoStoreInvocationContext;
|
||||
import org.keycloak.common.enums.SslRequired;
|
||||
|
@ -914,6 +913,7 @@ public class RealmAdapter extends AbstractMongoAdapter<MongoRealmEntity> impleme
|
|||
|
||||
identityProviderModel.setProviderId(entity.getProviderId());
|
||||
identityProviderModel.setAlias(entity.getAlias());
|
||||
identityProviderModel.setDisplayName(entity.getDisplayName());
|
||||
identityProviderModel.setInternalId(entity.getInternalId());
|
||||
Map<String, String> config = entity.getConfig();
|
||||
Map<String, String> copy = new HashMap<>();
|
||||
|
@ -950,6 +950,7 @@ public class RealmAdapter extends AbstractMongoAdapter<MongoRealmEntity> impleme
|
|||
|
||||
entity.setInternalId(KeycloakModelUtils.generateId());
|
||||
entity.setAlias(identityProvider.getAlias());
|
||||
entity.setDisplayName(identityProvider.getDisplayName());
|
||||
entity.setProviderId(identityProvider.getProviderId());
|
||||
entity.setEnabled(identityProvider.isEnabled());
|
||||
entity.setTrustEmail(identityProvider.isTrustEmail());
|
||||
|
@ -980,6 +981,7 @@ public class RealmAdapter extends AbstractMongoAdapter<MongoRealmEntity> impleme
|
|||
for (IdentityProviderEntity entity : this.realm.getIdentityProviders()) {
|
||||
if (entity.getInternalId().equals(identityProvider.getInternalId())) {
|
||||
entity.setAlias(identityProvider.getAlias());
|
||||
entity.setDisplayName(identityProvider.getDisplayName());
|
||||
entity.setEnabled(identityProvider.isEnabled());
|
||||
entity.setTrustEmail(identityProvider.isTrustEmail());
|
||||
entity.setAuthenticateByDefault(identityProvider.isAuthenticateByDefault());
|
||||
|
|
|
@ -57,6 +57,8 @@ public class IdentityProviderModel implements Serializable {
|
|||
|
||||
private String postBrokerLoginFlowId;
|
||||
|
||||
private String displayName;
|
||||
|
||||
/**
|
||||
* <p>A map containing the configuration and properties for a specific identity provider instance and implementation. The items
|
||||
* in the map are understood by the identity provider implementation.</p>
|
||||
|
@ -70,6 +72,7 @@ public class IdentityProviderModel implements Serializable {
|
|||
this.internalId = model.getInternalId();
|
||||
this.providerId = model.getProviderId();
|
||||
this.alias = model.getAlias();
|
||||
this.displayName = model.getDisplayName();
|
||||
this.config = new HashMap<String, String>(model.getConfig());
|
||||
this.enabled = model.isEnabled();
|
||||
this.trustEmail = model.isTrustEmail();
|
||||
|
@ -169,5 +172,13 @@ public class IdentityProviderModel implements Serializable {
|
|||
public void setTrustEmail(boolean trustEmail) {
|
||||
this.trustEmail = trustEmail;
|
||||
}
|
||||
|
||||
public String getDisplayName() {
|
||||
return displayName;
|
||||
}
|
||||
|
||||
public void setDisplayName(String displayName) {
|
||||
this.displayName = displayName;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -26,6 +26,7 @@ public class IdentityProviderEntity {
|
|||
|
||||
private String internalId;
|
||||
private String alias;
|
||||
private String displayName;
|
||||
private String providerId;
|
||||
private String name;
|
||||
private boolean enabled;
|
||||
|
@ -134,6 +135,14 @@ public class IdentityProviderEntity {
|
|||
this.trustEmail = trustEmail;
|
||||
}
|
||||
|
||||
public String getDisplayName() {
|
||||
return displayName;
|
||||
}
|
||||
|
||||
public void setDisplayName(String displayName) {
|
||||
this.displayName = displayName;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
|
|
|
@ -18,6 +18,8 @@
|
|||
package org.keycloak.models.utils;
|
||||
|
||||
import org.bouncycastle.openssl.PEMWriter;
|
||||
import org.keycloak.broker.social.SocialIdentityProvider;
|
||||
import org.keycloak.broker.social.SocialIdentityProviderFactory;
|
||||
import org.keycloak.common.util.Base64Url;
|
||||
import org.keycloak.models.AuthenticationExecutionModel;
|
||||
import org.keycloak.models.AuthenticationFlowModel;
|
||||
|
@ -47,8 +49,6 @@ import org.keycloak.common.util.PemUtils;
|
|||
import org.keycloak.transaction.JtaTransactionManagerLookup;
|
||||
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
import javax.naming.InitialContext;
|
||||
import javax.sql.DataSource;
|
||||
import javax.transaction.InvalidTransactionException;
|
||||
import javax.transaction.SystemException;
|
||||
import javax.transaction.Transaction;
|
||||
|
@ -62,7 +62,6 @@ import java.security.PrivateKey;
|
|||
import java.security.PublicKey;
|
||||
import java.security.SecureRandom;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.sql.DriverManager;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
|
@ -70,7 +69,6 @@ import java.util.List;
|
|||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.function.Function;
|
||||
|
||||
/**
|
||||
* Set of helper methods, which are useful in various model implementations.
|
||||
|
@ -689,4 +687,21 @@ public final class KeycloakModelUtils {
|
|||
}
|
||||
|
||||
}
|
||||
|
||||
public static String getIdentityProviderDisplayName(KeycloakSession session, IdentityProviderModel provider) {
|
||||
String displayName = provider.getDisplayName();
|
||||
if (displayName != null && !displayName.isEmpty()) {
|
||||
return displayName;
|
||||
}
|
||||
|
||||
SocialIdentityProviderFactory providerFactory = (SocialIdentityProviderFactory) session.getKeycloakSessionFactory()
|
||||
.getProviderFactory(SocialIdentityProvider.class, provider.getProviderId());
|
||||
if (providerFactory != null) {
|
||||
return providerFactory.getName();
|
||||
} else {
|
||||
return provider.getAlias();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
|
@ -627,6 +627,7 @@ public class ModelToRepresentation {
|
|||
providerRep.setInternalId(identityProviderModel.getInternalId());
|
||||
providerRep.setProviderId(identityProviderModel.getProviderId());
|
||||
providerRep.setAlias(identityProviderModel.getAlias());
|
||||
providerRep.setDisplayName(identityProviderModel.getDisplayName());
|
||||
providerRep.setEnabled(identityProviderModel.isEnabled());
|
||||
providerRep.setStoreToken(identityProviderModel.isStoreToken());
|
||||
providerRep.setTrustEmail(identityProviderModel.isTrustEmail());
|
||||
|
|
|
@ -1524,6 +1524,7 @@ public class RepresentationToModel {
|
|||
|
||||
identityProviderModel.setInternalId(representation.getInternalId());
|
||||
identityProviderModel.setAlias(representation.getAlias());
|
||||
identityProviderModel.setDisplayName(representation.getDisplayName());
|
||||
identityProviderModel.setProviderId(representation.getProviderId());
|
||||
identityProviderModel.setEnabled(representation.isEnabled());
|
||||
identityProviderModel.setTrustEmail(representation.isTrustEmail());
|
||||
|
|
|
@ -22,6 +22,7 @@ import org.keycloak.models.IdentityProviderModel;
|
|||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.UserModel;
|
||||
import org.keycloak.models.utils.KeycloakModelUtils;
|
||||
import org.keycloak.services.resources.AccountService;
|
||||
import org.keycloak.services.Urls;
|
||||
|
||||
|
@ -70,7 +71,8 @@ public class AccountFederatedIdentityBean {
|
|||
.queryParam("stateChecker", stateChecker)
|
||||
.build().toString();
|
||||
|
||||
FederatedIdentityEntry entry = new FederatedIdentityEntry(identity, provider.getAlias(), provider.getAlias(), actionUrl,
|
||||
String displayName = KeycloakModelUtils.getIdentityProviderDisplayName(session, provider);
|
||||
FederatedIdentityEntry entry = new FederatedIdentityEntry(identity, displayName, provider.getAlias(), provider.getAlias(), actionUrl,
|
||||
provider.getConfig() != null ? provider.getConfig().get("guiOrder") : null);
|
||||
orderedSet.add(entry);
|
||||
}
|
||||
|
@ -106,10 +108,12 @@ public class AccountFederatedIdentityBean {
|
|||
private final String providerName;
|
||||
private final String actionUrl;
|
||||
private final String guiOrder;
|
||||
private final String displayName;
|
||||
|
||||
public FederatedIdentityEntry(FederatedIdentityModel federatedIdentityModel, String providerId, String providerName, String actionUrl, String guiOrder
|
||||
) {
|
||||
public FederatedIdentityEntry(FederatedIdentityModel federatedIdentityModel, String displayName, String providerId,
|
||||
String providerName, String actionUrl, String guiOrder) {
|
||||
this.federatedIdentityModel = federatedIdentityModel;
|
||||
this.displayName = displayName;
|
||||
this.providerId = providerId;
|
||||
this.providerName = providerName;
|
||||
this.actionUrl = actionUrl;
|
||||
|
@ -143,6 +147,11 @@ public class AccountFederatedIdentityBean {
|
|||
public String getGuiOrder() {
|
||||
return guiOrder;
|
||||
}
|
||||
|
||||
public String getDisplayName() {
|
||||
return displayName;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public static class IdentityProviderComparator implements Comparator<FederatedIdentityEntry> {
|
||||
|
|
|
@ -250,7 +250,7 @@ public class FreeMarkerLoginFormsProvider implements LoginFormsProvider {
|
|||
|
||||
List<IdentityProviderModel> identityProviders = realm.getIdentityProviders();
|
||||
identityProviders = LoginFormsUtil.filterIdentityProviders(identityProviders, session, realm, attributes, formData);
|
||||
attributes.put("social", new IdentityProviderBean(realm, identityProviders, baseUri, uriInfo));
|
||||
attributes.put("social", new IdentityProviderBean(realm, session, identityProviders, baseUri, uriInfo));
|
||||
|
||||
attributes.put("url", new UrlBean(realm, theme, baseUri, this.actionUri));
|
||||
|
||||
|
@ -398,7 +398,7 @@ public class FreeMarkerLoginFormsProvider implements LoginFormsProvider {
|
|||
|
||||
List<IdentityProviderModel> identityProviders = realm.getIdentityProviders();
|
||||
identityProviders = LoginFormsUtil.filterIdentityProviders(identityProviders, session, realm, attributes, formData);
|
||||
attributes.put("social", new IdentityProviderBean(realm, identityProviders, baseUri, uriInfo));
|
||||
attributes.put("social", new IdentityProviderBean(realm, session, identityProviders, baseUri, uriInfo));
|
||||
|
||||
attributes.put("url", new UrlBean(realm, theme, baseUri, this.actionUri));
|
||||
attributes.put("requiredActionUrl", new RequiredActionUrlFormatterMethod(realm, baseUri));
|
||||
|
@ -425,7 +425,6 @@ public class FreeMarkerLoginFormsProvider implements LoginFormsProvider {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public Response createLogin() {
|
||||
return createResponse(LoginFormsPages.LOGIN);
|
||||
|
|
|
@ -17,7 +17,9 @@
|
|||
package org.keycloak.forms.login.freemarker.model;
|
||||
|
||||
import org.keycloak.models.IdentityProviderModel;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.utils.KeycloakModelUtils;
|
||||
import org.keycloak.services.Urls;
|
||||
|
||||
import javax.ws.rs.core.UriInfo;
|
||||
|
@ -35,12 +37,13 @@ import java.util.TreeSet;
|
|||
public class IdentityProviderBean {
|
||||
|
||||
private boolean displaySocial;
|
||||
|
||||
private List<IdentityProvider> providers;
|
||||
private RealmModel realm;
|
||||
private final KeycloakSession session;
|
||||
|
||||
public IdentityProviderBean(RealmModel realm, List<IdentityProviderModel> identityProviders, URI baseURI, UriInfo uriInfo) {
|
||||
public IdentityProviderBean(RealmModel realm, KeycloakSession session, List<IdentityProviderModel> identityProviders, URI baseURI, UriInfo uriInfo) {
|
||||
this.realm = realm;
|
||||
this.session = session;
|
||||
|
||||
if (!identityProviders.isEmpty()) {
|
||||
Set<IdentityProvider> orderedSet = new TreeSet<>(IdentityProviderComparator.INSTANCE);
|
||||
|
@ -59,7 +62,10 @@ public class IdentityProviderBean {
|
|||
|
||||
private void addIdentityProvider(Set<IdentityProvider> orderedSet, RealmModel realm, URI baseURI, IdentityProviderModel identityProvider) {
|
||||
String loginUrl = Urls.identityProviderAuthnRequest(baseURI, identityProvider.getAlias(), realm.getName()).toString();
|
||||
orderedSet.add(new IdentityProvider(identityProvider.getAlias(), identityProvider.getProviderId(), loginUrl,
|
||||
String displayName = KeycloakModelUtils.getIdentityProviderDisplayName(session, identityProvider);
|
||||
|
||||
orderedSet.add(new IdentityProvider(identityProvider.getAlias(),
|
||||
displayName, identityProvider.getProviderId(), loginUrl,
|
||||
identityProvider.getConfig() != null ? identityProvider.getConfig().get("guiOrder") : null));
|
||||
}
|
||||
|
||||
|
@ -77,9 +83,11 @@ public class IdentityProviderBean {
|
|||
private final String providerId; // This refer to providerType (facebook, google, etc.)
|
||||
private final String loginUrl;
|
||||
private final String guiOrder;
|
||||
private final String displayName;
|
||||
|
||||
public IdentityProvider(String alias, String providerId, String loginUrl, String guiOrder) {
|
||||
public IdentityProvider(String alias, String displayName, String providerId, String loginUrl, String guiOrder) {
|
||||
this.alias = alias;
|
||||
this.displayName = displayName;
|
||||
this.providerId = providerId;
|
||||
this.loginUrl = loginUrl;
|
||||
this.guiOrder = guiOrder;
|
||||
|
@ -100,6 +108,10 @@ public class IdentityProviderBean {
|
|||
public String getGuiOrder() {
|
||||
return guiOrder;
|
||||
}
|
||||
|
||||
public String getDisplayName() {
|
||||
return displayName;
|
||||
}
|
||||
}
|
||||
|
||||
public static class IdentityProviderComparator implements Comparator<IdentityProvider> {
|
||||
|
@ -112,7 +124,7 @@ public class IdentityProviderBean {
|
|||
|
||||
@Override
|
||||
public int compare(IdentityProvider o1, IdentityProvider o2) {
|
||||
|
||||
|
||||
int o1order = parseOrder(o1);
|
||||
int o2order = parseOrder(o2);
|
||||
|
||||
|
@ -120,7 +132,7 @@ public class IdentityProviderBean {
|
|||
return 1;
|
||||
else if (o1order < o2order)
|
||||
return -1;
|
||||
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
|
@ -134,6 +146,5 @@ public class IdentityProviderBean {
|
|||
}
|
||||
return 10000;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -32,32 +32,32 @@ public class IdentityProviderBeanTest {
|
|||
@Test
|
||||
public void testIdentityProviderComparator() {
|
||||
|
||||
IdentityProvider o1 = new IdentityProvider("alias1", "id1", "ur1", null);
|
||||
IdentityProvider o2 = new IdentityProvider("alias2", "id2", "ur2", null);
|
||||
IdentityProvider o1 = new IdentityProvider("alias1", "displayName1", "id1", "ur1", null);
|
||||
IdentityProvider o2 = new IdentityProvider("alias2", "displayName2", "id2", "ur2", null);
|
||||
|
||||
// guiOrder not defined at any object - first is always lower
|
||||
Assert.assertEquals(1, IdentityProviderComparator.INSTANCE.compare(o1, o2));
|
||||
Assert.assertEquals(1, IdentityProviderComparator.INSTANCE.compare(o2, o1));
|
||||
|
||||
// guiOrder is not a number so it is same as not defined - first is always lower
|
||||
o1 = new IdentityProvider("alias1", "id1", "ur1", "not a number");
|
||||
o1 = new IdentityProvider("alias1", "displayName1", "id1", "ur1", "not a number");
|
||||
Assert.assertEquals(1, IdentityProviderComparator.INSTANCE.compare(o1, o2));
|
||||
Assert.assertEquals(1, IdentityProviderComparator.INSTANCE.compare(o2, o1));
|
||||
|
||||
// guiOrder is defined for one only to it is always first
|
||||
o1 = new IdentityProvider("alias1", "id1", "ur1", "0");
|
||||
o1 = new IdentityProvider("alias1", "displayName1", "id1", "ur1", "0");
|
||||
Assert.assertEquals(-1, IdentityProviderComparator.INSTANCE.compare(o1, o2));
|
||||
Assert.assertEquals(1, IdentityProviderComparator.INSTANCE.compare(o2, o1));
|
||||
|
||||
// guiOrder is defined for both but is same - first is always lower
|
||||
o1 = new IdentityProvider("alias1", "id1", "ur1", "0");
|
||||
o2 = new IdentityProvider("alias2", "id2", "ur2", "0");
|
||||
o1 = new IdentityProvider("alias1", "displayName1", "id1", "ur1", "0");
|
||||
o2 = new IdentityProvider("alias2", "displayName2", "id2", "ur2", "0");
|
||||
Assert.assertEquals(1, IdentityProviderComparator.INSTANCE.compare(o1, o2));
|
||||
Assert.assertEquals(1, IdentityProviderComparator.INSTANCE.compare(o2, o1));
|
||||
|
||||
// guiOrder is reflected
|
||||
o1 = new IdentityProvider("alias1", "id1", "ur1", "0");
|
||||
o2 = new IdentityProvider("alias2", "id2", "ur2", "1");
|
||||
o1 = new IdentityProvider("alias1", "displayName1", "id1", "ur1", "0");
|
||||
o2 = new IdentityProvider("alias2", "displayName2", "id2", "ur2", "1");
|
||||
Assert.assertEquals(-1, IdentityProviderComparator.INSTANCE.compare(o1, o2));
|
||||
Assert.assertEquals(1, IdentityProviderComparator.INSTANCE.compare(o2, o1));
|
||||
|
||||
|
|
|
@ -38,6 +38,7 @@ import org.keycloak.testsuite.pages.AppPage.RequestType;
|
|||
import org.keycloak.testsuite.pages.ErrorPage;
|
||||
import org.keycloak.testsuite.pages.LoginPage;
|
||||
import org.keycloak.testsuite.pages.RegisterPage;
|
||||
import org.keycloak.testsuite.util.IdentityProviderBuilder;
|
||||
import org.openqa.selenium.By;
|
||||
import org.openqa.selenium.WebDriver;
|
||||
|
||||
|
@ -75,6 +76,20 @@ public class AccountTest extends TestRealmKeycloakTest {
|
|||
.password("password")
|
||||
.build();
|
||||
|
||||
testRealm.addIdentityProvider(IdentityProviderBuilder.create()
|
||||
.providerId("github")
|
||||
.alias("github")
|
||||
.build());
|
||||
testRealm.addIdentityProvider(IdentityProviderBuilder.create()
|
||||
.providerId("saml")
|
||||
.alias("mysaml")
|
||||
.build());
|
||||
testRealm.addIdentityProvider(IdentityProviderBuilder.create()
|
||||
.providerId("oidc")
|
||||
.alias("myoidc")
|
||||
.displayName("MyOIDC")
|
||||
.build());
|
||||
|
||||
RealmBuilder.edit(testRealm)
|
||||
.user(user2);
|
||||
}
|
||||
|
@ -790,4 +805,13 @@ public class AccountTest extends TestRealmKeycloakTest {
|
|||
events.clear();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testIdentityProviderCapitalization(){
|
||||
loginPage.open();
|
||||
Assert.assertEquals("GitHub", loginPage.findSocialButton("github").getText());
|
||||
Assert.assertEquals("mysaml", loginPage.findSocialButton("mysaml").getText());
|
||||
Assert.assertEquals("MyOIDC", loginPage.findSocialButton("myoidc").getText());
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -20,9 +20,6 @@ package org.keycloak.testsuite.admin;
|
|||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
@ -31,16 +28,12 @@ import org.junit.Before;
|
|||
import org.junit.Rule;
|
||||
import org.keycloak.admin.client.resource.RealmResource;
|
||||
import org.keycloak.events.log.JBossLoggingEventListenerProviderFactory;
|
||||
import org.keycloak.representations.idm.ClientRepresentation;
|
||||
import org.keycloak.representations.idm.IdentityProviderRepresentation;
|
||||
import org.keycloak.representations.idm.RealmRepresentation;
|
||||
import org.keycloak.testsuite.TestRealmKeycloakTest;
|
||||
import org.keycloak.testsuite.events.EventsListenerProviderFactory;
|
||||
import org.keycloak.testsuite.util.AssertAdminEvents;
|
||||
import org.keycloak.util.JsonSerialization;
|
||||
|
||||
import static org.junit.Assert.assertArrayEquals;
|
||||
|
||||
/**
|
||||
* This class adapts the functionality from the old testsuite to make tests
|
||||
* easier to port.
|
||||
|
@ -109,4 +102,4 @@ public abstract class AbstractAdminTest extends TestRealmKeycloakTest {
|
|||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
|
@ -205,6 +205,7 @@ public class ConsentsTest extends AbstractKeycloakTest {
|
|||
IdentityProviderRepresentation identityProviderRepresentation = new IdentityProviderRepresentation();
|
||||
|
||||
identityProviderRepresentation.setAlias(alias);
|
||||
identityProviderRepresentation.setDisplayName(providerId);
|
||||
identityProviderRepresentation.setProviderId(providerId);
|
||||
identityProviderRepresentation.setEnabled(true);
|
||||
|
||||
|
|
|
@ -177,6 +177,7 @@ public class IdentityProviderTest extends AbstractAdminTest {
|
|||
IdentityProviderRepresentation idp = new IdentityProviderRepresentation();
|
||||
|
||||
idp.setAlias(id);
|
||||
idp.setDisplayName(id);
|
||||
idp.setProviderId(providerId);
|
||||
idp.setEnabled(true);
|
||||
if (config != null) {
|
||||
|
|
|
@ -1440,7 +1440,8 @@ public class PermissionsTest extends AbstractKeycloakTest {
|
|||
}, Resource.IDENTITY_PROVIDER, false);
|
||||
invoke(new InvocationWithResponse() {
|
||||
public void invoke(RealmResource realm, AtomicReference<Response> response) {
|
||||
response.set(realm.identityProviders().create(IdentityProviderBuilder.create().providerId("nosuch").alias("foo").build()));
|
||||
response.set(realm.identityProviders().create(IdentityProviderBuilder.create().providerId("nosuch")
|
||||
.displayName("nosuch-foo").alias("foo").build()));
|
||||
}
|
||||
}, Resource.IDENTITY_PROVIDER, true);
|
||||
|
||||
|
|
|
@ -126,6 +126,7 @@ public abstract class AbstractBrokerTest extends AbstractKeycloakTest {
|
|||
IdentityProviderRepresentation identityProviderRepresentation = new IdentityProviderRepresentation();
|
||||
|
||||
identityProviderRepresentation.setAlias(alias);
|
||||
identityProviderRepresentation.setDisplayName(providerId);
|
||||
identityProviderRepresentation.setProviderId(providerId);
|
||||
identityProviderRepresentation.setEnabled(true);
|
||||
|
||||
|
|
|
@ -23,10 +23,12 @@ import org.jboss.resteasy.client.jaxrs.engines.ApacheHttpClient4Engine;
|
|||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
import org.keycloak.adapters.HttpClientBuilder;
|
||||
import org.keycloak.representations.idm.RealmRepresentation;
|
||||
import org.keycloak.testsuite.pages.LoginPage;
|
||||
|
||||
import javax.ws.rs.core.Response;
|
||||
import org.jboss.arquillian.graphene.page.Page;
|
||||
import org.keycloak.testsuite.util.IdentityProviderBuilder;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:gerbermichi@me.com">Michael Gerber</a>
|
||||
|
@ -37,6 +39,24 @@ public class LoginPageTest extends AbstractI18NTest {
|
|||
@Page
|
||||
protected LoginPage loginPage;
|
||||
|
||||
@Override
|
||||
public void configureTestRealm(RealmRepresentation testRealm) {
|
||||
testRealm.addIdentityProvider(IdentityProviderBuilder.create()
|
||||
.providerId("github")
|
||||
.alias("github")
|
||||
.build());
|
||||
testRealm.addIdentityProvider(IdentityProviderBuilder.create()
|
||||
.providerId("saml")
|
||||
.alias("mysaml")
|
||||
.build());
|
||||
testRealm.addIdentityProvider(IdentityProviderBuilder.create()
|
||||
.providerId("oidc")
|
||||
.alias("myoidc")
|
||||
.displayName("MyOIDC")
|
||||
.build());
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
public void languageDropdown() {
|
||||
loginPage.open();
|
||||
|
@ -87,4 +107,13 @@ public class LoginPageTest extends AbstractI18NTest {
|
|||
response = client.target(driver.getCurrentUrl()).request().acceptLanguage("en").get();
|
||||
Assert.assertTrue(response.readEntity(String.class).contains("Log in to test"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testIdentityProviderCapitalization(){
|
||||
loginPage.open();
|
||||
Assert.assertEquals("GitHub", loginPage.findSocialButton("github").getText());
|
||||
Assert.assertEquals("mysaml", loginPage.findSocialButton("mysaml").getText());
|
||||
Assert.assertEquals("MyOIDC", loginPage.findSocialButton("myoidc").getText());
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -44,6 +44,11 @@ public class IdentityProviderBuilder {
|
|||
return this;
|
||||
}
|
||||
|
||||
public IdentityProviderBuilder displayName(String displayName) {
|
||||
rep.setDisplayName(displayName);
|
||||
return this;
|
||||
}
|
||||
|
||||
public IdentityProviderRepresentation build() {
|
||||
return rep;
|
||||
}
|
||||
|
|
|
@ -94,6 +94,7 @@
|
|||
{
|
||||
"alias" : "model-saml-signed-idp",
|
||||
"providerId" : "saml",
|
||||
"displayName": "My SAML",
|
||||
"enabled": true,
|
||||
"config": {
|
||||
"singleSignOnServiceUrl": "http://localhost:8082/auth/realms/realm-with-saml-identity-provider/protocol/saml",
|
||||
|
@ -157,6 +158,7 @@
|
|||
{
|
||||
"alias" : "model-oidc-idp",
|
||||
"providerId" : "oidc",
|
||||
"displayName": "My OIDC",
|
||||
"enabled": false,
|
||||
"authenticateByDefault" : "false",
|
||||
"config": {
|
||||
|
@ -172,6 +174,7 @@
|
|||
{
|
||||
"alias" : "kc-oidc-idp",
|
||||
"providerId" : "keycloak-oidc",
|
||||
"displayName": "My Keycloak OIDC",
|
||||
"enabled": true,
|
||||
"storeToken" : true,
|
||||
"addReadTokenRoleOnCreate": true,
|
||||
|
|
|
@ -11,7 +11,7 @@
|
|||
<#list federatedIdentity.identities as identity>
|
||||
<div class="form-group">
|
||||
<div class="col-sm-2 col-md-2">
|
||||
<label for="${identity.providerId!}" class="control-label">${identity.providerName!}</label>
|
||||
<label for="${identity.providerId!}" class="control-label">${identity.displayName!}</label>
|
||||
</div>
|
||||
<div class="col-sm-5 col-md-5">
|
||||
<input disabled="true" class="form-control" value="${identity.userName!}">
|
||||
|
|
|
@ -418,7 +418,9 @@ post-broker-login-flow=Post Login Flow
|
|||
redirect-uri=Redirect URI
|
||||
redirect-uri.tooltip=The redirect uri to use when configuring the identity provider.
|
||||
alias=Alias
|
||||
display-name=Display Name
|
||||
identity-provider.alias.tooltip=The alias uniquely identifies an identity provider and it is also used to build the redirect uri.
|
||||
identity-provider.display-name.tooltip=Friendly name for Identity Providers.
|
||||
identity-provider.enabled.tooltip=Enable/disable this identity provider.
|
||||
authenticate-by-default=Authenticate by Default
|
||||
identity-provider.authenticate-by-default.tooltip=Indicates if this provider should be tried by default for authentication even before displaying login screen.
|
||||
|
|
16
themes/src/main/resources/theme/base/admin/resources/js/controllers/realm.js
Executable file → Normal file
16
themes/src/main/resources/theme/base/admin/resources/js/controllers/realm.js
Executable file → Normal file
|
@ -768,11 +768,19 @@ module.controller('RealmIdentityProviderCtrl', function($scope, $filter, $upload
|
|||
if (instance && instance.alias) {
|
||||
$scope.identityProvider = angular.copy(instance);
|
||||
$scope.newIdentityProvider = false;
|
||||
for (var i in serverInfo.identityProviders) {
|
||||
var provider = serverInfo.identityProviders[i];
|
||||
|
||||
if (provider.id == instance.providerId) {
|
||||
$scope.provider = provider;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$scope.identityProvider = {};
|
||||
$scope.identityProvider.config = {};
|
||||
$scope.identityProvider.alias = providerFactory.id;
|
||||
$scope.identityProvider.providerId = providerFactory.id;
|
||||
|
||||
$scope.identityProvider.enabled = true;
|
||||
$scope.identityProvider.authenticateByDefault = false;
|
||||
$scope.identityProvider.firstBrokerLoginFlowAlias = 'first broker login';
|
||||
|
@ -836,7 +844,6 @@ module.controller('RealmIdentityProviderCtrl', function($scope, $filter, $upload
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
$scope.uploadFile = function() {
|
||||
if (!$scope.identityProvider.alias) {
|
||||
Notifications.error("You must specify an alias");
|
||||
|
@ -906,13 +913,12 @@ module.controller('RealmIdentityProviderCtrl', function($scope, $filter, $upload
|
|||
|
||||
for (var i in $scope.allProviders) {
|
||||
var provider = $scope.allProviders[i];
|
||||
|
||||
if (provider.groupName == 'Social' && (provider.id == configProvidedId)) {
|
||||
$scope.allProviders.splice(i, 1);
|
||||
break;
|
||||
if (provider.id == configProvidedId) {
|
||||
configuredProviders[j].provider = provider;
|
||||
}
|
||||
}
|
||||
}
|
||||
$scope.configuredProviders = angular.copy(configuredProviders);
|
||||
}
|
||||
}, true);
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<div class="col-sm-9 col-md-10 col-sm-push-3 col-md-push-2">
|
||||
<ol class="breadcrumb">
|
||||
<li><a href="#/realms/{{realm.realm}}/identity-provider-settings">{{:: 'identity-providers' | translate}}</a></li>
|
||||
<li><a href="#/realms/{{realm.realm}}/identity-provider-settings/provider/{{identityProvider.providerId}}/{{identityProvider.alias}}">{{identityProvider.alias}}</a></li>
|
||||
<li><a href="#/realms/{{realm.realm}}/identity-provider-settings/provider/{{identityProvider.providerId}}/{{identityProvider.alias}}">{{identityProvider.displayName}}</a></li>
|
||||
<li><a href="#/realms/{{realm.realm}}/identity-provider-mappers/{{identityProvider.alias}}/mappers">{{:: 'identity-provider-mappers' | translate}}</a></li>
|
||||
<li class="active" data-ng-show="create">{{:: 'create-identity-provider-mapper' | translate}}</li>
|
||||
<li class="active" data-ng-hide="create">{{mapper.name|capitalize}}</li>
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<div class="col-sm-9 col-md-10 col-sm-push-3 col-md-push-2">
|
||||
<ol class="breadcrumb">
|
||||
<li><a href="#/realms/{{realm.realm}}/identity-provider-settings">{{:: 'identity-providers' | translate}}</a></li>
|
||||
<li>{{identityProvider.alias}}</li>
|
||||
<li>{{identityProvider.displayName}}</li>
|
||||
</ol>
|
||||
|
||||
<kc-tabs-identity-provider></kc-tabs-identity-provider>
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<div class="col-sm-9 col-md-10 col-sm-push-3 col-md-push-2" data-ng-init="initProvider()">
|
||||
<ol class="breadcrumb">
|
||||
<li><a href="#/realms/{{realm.realm}}/identity-provider-settings">{{:: 'identity-providers' | translate}}</a></li>
|
||||
<li>{{identityProvider.alias}}</li>
|
||||
<li>{{identityProvider.displayName}}</li>
|
||||
</ol>
|
||||
|
||||
<kc-tabs-identity-provider></kc-tabs-identity-provider>
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
<div class="col-sm-9 col-md-10 col-sm-push-3 col-md-push-2">
|
||||
<ol class="breadcrumb">
|
||||
<li><a href="#/realms/{{realm.realm}}/identity-provider-settings">{{:: 'identity-providers' | translate}}</a></li>
|
||||
<li>{{identityProvider.alias}}</li>
|
||||
<li data-ng-show="!newIdentityProvider && identityProvider.displayName">{{identityProvider.displayName}}</li>
|
||||
<li data-ng-show="!newIdentityProvider && !identityProvider.displayName">{{identityProvider.alias}}</li>
|
||||
<li data-ng-show="newIdentityProvider">{{:: 'add-identity-provider' | translate}}</li>
|
||||
</ol>
|
||||
|
||||
<kc-tabs-identity-provider></kc-tabs-identity-provider>
|
||||
|
@ -27,6 +29,13 @@
|
|||
</div>
|
||||
<kc-tooltip>{{:: 'identity-provider.alias.tooltip' | translate}}</kc-tooltip>
|
||||
</div>
|
||||
<div class="form-group clearfix">
|
||||
<label class="col-md-2 control-label" for="displayName"> {{:: 'display-name' | translate}}</label>
|
||||
<div class="col-md-6">
|
||||
<input class="form-control" id="displayName" type="text" ng-model="identityProvider.displayName">
|
||||
</div>
|
||||
<kc-tooltip>{{:: 'identity-provider.display-name.tooltip' | translate}}</kc-tooltip>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="col-md-2 control-label" for="enabled">{{:: 'enabled' | translate}}</label>
|
||||
<div class="col-md-6">
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
<div class="col-sm-9 col-md-10 col-sm-push-3 col-md-push-2" data-ng-init="initSamlProvider()">
|
||||
<ol class="breadcrumb">
|
||||
<li><a href="#/realms/{{realm.realm}}/identity-provider-settings">{{:: 'identity-providers' | translate}}</a></li>
|
||||
<li>{{identityProvider.alias}}</li>
|
||||
<li data-ng-show="!newIdentityProvider && identityProvider.displayName">{{identityProvider.displayName}}</li>
|
||||
<li data-ng-show="!newIdentityProvider && !identityProvider.displayName">{{identityProvider.alias}}</li>
|
||||
<li data-ng-show="newIdentityProvider">{{:: 'add-identity-provider' | translate}}</li>
|
||||
</ol>
|
||||
|
||||
<kc-tabs-identity-provider></kc-tabs-identity-provider>
|
||||
|
@ -24,6 +26,13 @@
|
|||
</div>
|
||||
<kc-tooltip>{{:: 'identity-provider.alias.tooltip' | translate}}</kc-tooltip>
|
||||
</div>
|
||||
<div class="form-group clearfix">
|
||||
<label class="col-md-2 control-label" for="displayName"> {{:: 'display-name' | translate}}</label>
|
||||
<div class="col-md-6">
|
||||
<input class="form-control" id="displayName" type="text" ng-model="identityProvider.displayName">
|
||||
</div>
|
||||
<kc-tooltip>{{:: 'identity-provider.display-name.tooltip' | translate}}</kc-tooltip>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="col-md-2 control-label" for="enabled">{{:: 'enabled' | translate}}</label>
|
||||
<div class="col-md-6">
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
<div class="col-sm-9 col-md-10 col-sm-push-3 col-md-push-2">
|
||||
<ol class="breadcrumb">
|
||||
<li><a href="#/realms/{{realm.realm}}/identity-provider-settings">{{:: 'identity-providers' | translate}}</a></li>
|
||||
<li>{{identityProvider.alias}}</li>
|
||||
<li data-ng-hide="newIdentityProvider">{{provider.name}}</li>
|
||||
<li data-ng-show="newIdentityProvider">{{:: 'add-identity-provider' | translate}}</li>
|
||||
</ol>
|
||||
|
||||
<kc-tabs-identity-provider></kc-tabs-identity-provider>
|
||||
|
|
|
@ -54,7 +54,11 @@
|
|||
<tbody>
|
||||
<tr ng-repeat="identityProvider in configuredProviders">
|
||||
<td>
|
||||
<a href="#/realms/{{realm.realm}}/identity-provider-settings/provider/{{identityProvider.providerId}}/{{identityProvider.alias}}">{{identityProvider.alias}}</a>
|
||||
<a href="#/realms/{{realm.realm}}/identity-provider-settings/provider/{{identityProvider.providerId}}/{{identityProvider.alias}}">
|
||||
<span data-ng-show="identityProvider.displayName">{{identityProvider.displayName}}</span>
|
||||
<span data-ng-show="!identityProvider.displayName && identityProvider.provider.groupName == 'Social'">{{identityProvider.provider.name}}</span>
|
||||
<span data-ng-show="!identityProvider.displayName && identityProvider.provider.groupName != 'Social'">{{identityProvider.alias}}</span>
|
||||
</a>
|
||||
</td>
|
||||
<td>{{identityProvider.providerId}}</td>
|
||||
<td translate="{{identityProvider.enabled}}"></td>
|
||||
|
|
|
@ -1,6 +1,9 @@
|
|||
<div data-ng-controller="IdentityProviderTabCtrl">
|
||||
<h1 data-ng-hide="path[0] == 'create'">
|
||||
{{identityProvider.alias|capitalize}}
|
||||
<span data-ng-show="identityProvider.displayName">{{identityProvider.displayName}}</span>
|
||||
<span data-ng-show="!identityProvider.displayName && provider.groupName == 'Social'">{{provider.name}}</span>
|
||||
<span data-ng-show="!identityProvider.displayName && provider.groupName != 'Social'">{{identityProvider.alias}}</span>
|
||||
|
||||
<i class="pficon pficon-delete clickable" data-ng-hide="newIdentityProvider || changed" data-ng-click="removeIdentityProvider()"></i>
|
||||
</h1>
|
||||
<h1 data-ng-show="path[0] == 'create'">{{:: 'add-identity-provider' | translate}}</h1>
|
||||
|
|
|
@ -70,7 +70,7 @@
|
|||
<div id="kc-social-providers">
|
||||
<ul>
|
||||
<#list social.providers as p>
|
||||
<li><a href="${p.loginUrl}" id="zocial-${p.alias}" class="zocial ${p.providerId}"> <span class="text">${p.alias}</span></a></li>
|
||||
<li><a href="${p.loginUrl}" id="zocial-${p.alias}" class="zocial ${p.providerId}"> <span class="text">${p.displayName}</span></a></li>
|
||||
</#list>
|
||||
</ul>
|
||||
</div>
|
||||
|
|
|
@ -229,9 +229,6 @@ ol#kc-totp-settings li:first-of-type {
|
|||
.zocial.google {
|
||||
background-color: #dd4b39 !important;
|
||||
}
|
||||
.zocial.google .text:after {
|
||||
content: "+";
|
||||
}
|
||||
|
||||
.zocial.facebook:hover,
|
||||
.zocial.github:hover,
|
||||
|
|
Loading…
Reference in a new issue