Completed the refactoring as Maven multi-module project and each project has been documented as the new project template advise. It also provide the avatar support (#19726)
parent
8c6f0d4337
commit
ee74c947d0
@ -1,10 +1,12 @@
|
||||
This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
# Changelog for "keycloak-extension-spi"
|
||||
|
||||
## [v0.2.0-SNAPSHOT]
|
||||
Completed the refactoring as Maven multi-module project and each project has been documented as the new project template advise. It also provide the avatar support (#19726)
|
||||
|
||||
## [v0.1.0-SNAPSHOT]
|
||||
- First release as Maven multi-module project and support for avatar (#19726)
|
||||
- Refactored as Maven multi-module project and support for avatar (#19726)
|
||||
|
||||
## [v0.0.1-SNAPSHOT]
|
||||
- First release (#19657, #19684)
|
||||
|
||||
|
||||
This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
@ -0,0 +1,26 @@
|
||||
# Acknowledgments
|
||||
|
||||
The projects leading to this software have received funding from a series of European Union programmes including:
|
||||
|
||||
- the Sixth Framework Programme for Research and Technological Development
|
||||
- [DILIGENT](https://cordis.europa.eu/project/id/004260) (grant no. 004260).
|
||||
- the Seventh Framework Programme for research, technological development and demonstration
|
||||
- [D4Science](https://cordis.europa.eu/project/id/212488) (grant no. 212488);
|
||||
- [D4Science-II](https://cordis.europa.eu/project/id/239019) (grant no.239019);
|
||||
- [ENVRI](https://cordis.europa.eu/project/id/283465) (grant no. 283465);
|
||||
- [iMarine](https://cordis.europa.eu/project/id/283644) (grant no. 283644);
|
||||
- [EUBrazilOpenBio](https://cordis.europa.eu/project/id/288754) (grant no. 288754).
|
||||
- the H2020 research and innovation programme
|
||||
- [SoBigData](https://cordis.europa.eu/project/id/654024) (grant no. 654024);
|
||||
- [PARTHENOS](https://cordis.europa.eu/project/id/654119) (grant no. 654119);
|
||||
- [EGI-Engage](https://cordis.europa.eu/project/id/654142) (grant no. 654142);
|
||||
- [ENVRI PLUS](https://cordis.europa.eu/project/id/654182) (grant no. 654182);
|
||||
- [BlueBRIDGE](https://cordis.europa.eu/project/id/675680) (grant no. 675680);
|
||||
- [PerformFISH](https://cordis.europa.eu/project/id/727610) (grant no. 727610);
|
||||
- [AGINFRA PLUS](https://cordis.europa.eu/project/id/731001) (grant no. 731001);
|
||||
- [DESIRA](https://cordis.europa.eu/project/id/818194) (grant no. 818194);
|
||||
- [ARIADNEplus](https://cordis.europa.eu/project/id/823914) (grant no. 823914);
|
||||
- [RISIS 2](https://cordis.europa.eu/project/id/824091) (grant no. 824091);
|
||||
- [EOSC-Pillar](https://cordis.europa.eu/project/id/857650) (grant no. 857650);
|
||||
- [Blue Cloud](https://cordis.europa.eu/project/id/862409) (grant no. 862409);
|
||||
- [SoBigData-PlusPlus](https://cordis.europa.eu/project/id/871042) (grant no. 871042);
|
@ -0,0 +1,11 @@
|
||||
This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
# Changelog for "avatar-importer"
|
||||
|
||||
## [v0.2.0-SNAPSHOT]
|
||||
|
||||
- First release. It provides avatar import from Identity Providers login. (#19726).<br>
|
||||
Currently supported IdPs:
|
||||
- Google
|
||||
- Facebook
|
||||
- LinkedIn
|
@ -0,0 +1,26 @@
|
||||
# Acknowledgments
|
||||
|
||||
The projects leading to this software have received funding from a series of European Union programmes including:
|
||||
|
||||
- the Sixth Framework Programme for Research and Technological Development
|
||||
- [DILIGENT](https://cordis.europa.eu/project/id/004260) (grant no. 004260).
|
||||
- the Seventh Framework Programme for research, technological development and demonstration
|
||||
- [D4Science](https://cordis.europa.eu/project/id/212488) (grant no. 212488);
|
||||
- [D4Science-II](https://cordis.europa.eu/project/id/239019) (grant no.239019);
|
||||
- [ENVRI](https://cordis.europa.eu/project/id/283465) (grant no. 283465);
|
||||
- [iMarine](https://cordis.europa.eu/project/id/283644) (grant no. 283644);
|
||||
- [EUBrazilOpenBio](https://cordis.europa.eu/project/id/288754) (grant no. 288754).
|
||||
- the H2020 research and innovation programme
|
||||
- [SoBigData](https://cordis.europa.eu/project/id/654024) (grant no. 654024);
|
||||
- [PARTHENOS](https://cordis.europa.eu/project/id/654119) (grant no. 654119);
|
||||
- [EGI-Engage](https://cordis.europa.eu/project/id/654142) (grant no. 654142);
|
||||
- [ENVRI PLUS](https://cordis.europa.eu/project/id/654182) (grant no. 654182);
|
||||
- [BlueBRIDGE](https://cordis.europa.eu/project/id/675680) (grant no. 675680);
|
||||
- [PerformFISH](https://cordis.europa.eu/project/id/727610) (grant no. 727610);
|
||||
- [AGINFRA PLUS](https://cordis.europa.eu/project/id/731001) (grant no. 731001);
|
||||
- [DESIRA](https://cordis.europa.eu/project/id/818194) (grant no. 818194);
|
||||
- [ARIADNEplus](https://cordis.europa.eu/project/id/823914) (grant no. 823914);
|
||||
- [RISIS 2](https://cordis.europa.eu/project/id/824091) (grant no. 824091);
|
||||
- [EOSC-Pillar](https://cordis.europa.eu/project/id/857650) (grant no. 857650);
|
||||
- [Blue Cloud](https://cordis.europa.eu/project/id/862409) (grant no. 862409);
|
||||
- [SoBigData-PlusPlus](https://cordis.europa.eu/project/id/871042) (grant no. 871042);
|
@ -0,0 +1,50 @@
|
||||
# Event Publisher Portal
|
||||
|
||||
**Avatar Importer** extends the [Keycloak](https://www.keycloak.org)'s Identity Provider (IdP) mapper SPI to import the avatar image defined inside the IdP used for the logn.
|
||||
|
||||
## Structure of the project
|
||||
|
||||
The source code is present in `src` folder.
|
||||
* Relevant information about how the repository is organized.
|
||||
* Description of the Maven modules (if any).
|
||||
* Any information needed to work with the code.
|
||||
|
||||
## Built With
|
||||
|
||||
* [OpenJDK](https://openjdk.java.net/) - The JDK used
|
||||
* [Maven](https://maven.apache.org/) - Dependency Management
|
||||
|
||||
## Documentation
|
||||
|
||||
This is one of the modules that composes the EAR deployment defined in the "brother" module [keycloak-d4science-spi](../keycloak-d4science-spi-ear/README.md).
|
||||
|
||||
To build the module's JAR file it is sufficient to type
|
||||
|
||||
mvn clean package
|
||||
|
||||
## Change log
|
||||
|
||||
See [CHANGELOG.md](CHANGELOG.md).
|
||||
|
||||
## Authors
|
||||
|
||||
* **Marco Lettere** ([Nubisware S.r.l.](http://www.nubisware.com))
|
||||
* **Mauro Mugnaini** ([Nubisware S.r.l.](http://www.nubisware.com))
|
||||
|
||||
## How to Cite this Software
|
||||
[Intentionally left blank]
|
||||
|
||||
## License
|
||||
|
||||
This project is licensed under the EUPL V.1.1 License - see the [LICENSE.md](LICENSE.md) file for details.
|
||||
|
||||
## About the gCube Framework
|
||||
This software is part of the [gCubeFramework](https://www.gcube-system.org/ "gCubeFramework"): an
|
||||
open-source software toolkit used for building and operating Hybrid Data
|
||||
Infrastructures enabling the dynamic deployment of Virtual Research Environments
|
||||
by favouring the realisation of reuse oriented policies.
|
||||
|
||||
The projects leading to this software have received funding from a series of European Union programmes see [FUNDING.md](FUNDING.md)
|
||||
|
||||
## Acknowledgments
|
||||
[Intentionally left blank]
|
@ -0,0 +1,33 @@
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<parent>
|
||||
<groupId>org.gcube</groupId>
|
||||
<artifactId>keycloak-d4science-spi-parent</artifactId>
|
||||
<version>0.1.0-SNAPSHOT</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>avatar-importer</artifactId>
|
||||
<packaging>jar</packaging>
|
||||
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.gcube</groupId>
|
||||
<artifactId>avatar-storage</artifactId>
|
||||
<version>${project.version}</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>dnsjava</groupId>
|
||||
<artifactId>dnsjava</artifactId>
|
||||
<version>3.2.2</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.googlecode.json-simple</groupId>
|
||||
<artifactId>json-simple</artifactId>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
</project>
|
@ -0,0 +1,271 @@
|
||||
package org.gcube.keycloak.oidc.avatar.importer;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.net.URL;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashSet;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
import org.apache.commons.lang.ArrayUtils;
|
||||
import org.gcube.keycloak.avatar.storage.AvatarStorageProvider;
|
||||
import org.gcube.keycloak.oidc.avatar.importer.libravatar.Libravatar;
|
||||
import org.gcube.keycloak.oidc.avatar.importer.libravatar.LibravatarDefaultImage;
|
||||
import org.gcube.keycloak.oidc.avatar.importer.libravatar.LibravatarException;
|
||||
import org.gcube.keycloak.oidc.avatar.importer.libravatar.LibravatarOptions;
|
||||
import org.jboss.logging.Logger;
|
||||
import org.json.simple.JSONArray;
|
||||
import org.json.simple.JSONObject;
|
||||
import org.json.simple.parser.JSONParser;
|
||||
import org.json.simple.parser.ParseException;
|
||||
import org.keycloak.broker.oidc.KeycloakOIDCIdentityProvider;
|
||||
import org.keycloak.broker.oidc.KeycloakOIDCIdentityProviderFactory;
|
||||
import org.keycloak.broker.oidc.OIDCIdentityProviderFactory;
|
||||
import org.keycloak.broker.oidc.mappers.AbstractClaimMapper;
|
||||
import org.keycloak.broker.provider.BrokeredIdentityContext;
|
||||
import org.keycloak.models.IdentityProviderMapperModel;
|
||||
import org.keycloak.models.IdentityProviderSyncMode;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.UserModel;
|
||||
import org.keycloak.provider.ProviderConfigProperty;
|
||||
import org.keycloak.provider.ProviderConfigurationBuilder;
|
||||
import org.keycloak.social.facebook.FacebookIdentityProviderFactory;
|
||||
import org.keycloak.social.google.GoogleIdentityProviderFactory;
|
||||
import org.keycloak.social.linkedin.LinkedInIdentityProviderFactory;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:mauro.mugnaini@nubisware.com">Mauro Mugnaini</a>
|
||||
*/
|
||||
public class AvatarImporter extends AbstractClaimMapper {
|
||||
|
||||
private static final Logger logger = Logger.getLogger(AvatarImporter.class);
|
||||
|
||||
public static final String[] PROVIDERS_WITH_PICTURE_CLAIM = {
|
||||
GoogleIdentityProviderFactory.PROVIDER_ID,
|
||||
OIDCIdentityProviderFactory.PROVIDER_ID,
|
||||
// BitbucketIdentityProviderFactory.PROVIDER_ID,
|
||||
KeycloakOIDCIdentityProviderFactory.PROVIDER_ID
|
||||
// GitHubIdentityProviderFactory.PROVIDER_ID,
|
||||
// GitLabIdentityProviderFactory.PROVIDER_ID,
|
||||
// InstagramIdentityProviderFactory.PROVIDER_ID,
|
||||
// MicrosoftIdentityProviderFactory.PROVIDER_ID,
|
||||
// OpenshiftV3IdentityProviderFactory.PROVIDER_ID,
|
||||
// OpenshiftV4IdentityProviderFactory.PROVIDER_ID,
|
||||
// PayPalIdentityProviderFactory.PROVIDER_ID,
|
||||
// StackoverflowIdentityProviderFactory.PROVIDER_ID,
|
||||
// TwitterIdentityProviderFactory.PROVIDER_ID
|
||||
};
|
||||
|
||||
public static final String[] PROVIDERS_WITH_SPECIFIC_CODE = {
|
||||
FacebookIdentityProviderFactory.PROVIDER_ID,
|
||||
LinkedInIdentityProviderFactory.PROVIDER_ID
|
||||
};
|
||||
|
||||
public static final String[] COMPATIBLE_PROVIDERS = (String[]) ArrayUtils.addAll(PROVIDERS_WITH_PICTURE_CLAIM,
|
||||
PROVIDERS_WITH_SPECIFIC_CODE);
|
||||
|
||||
private static final List<ProviderConfigProperty> configProperties;
|
||||
private static final Set<IdentityProviderSyncMode> IDENTITY_PROVIDER_SYNC_MODES = new HashSet<>(
|
||||
Arrays.asList(IdentityProviderSyncMode.values()));
|
||||
|
||||
public static final Integer DEFAULT_AVATAR_SIZE = 160;
|
||||
|
||||
public static final String USE_LIBRAVATAR_PROPERTY = "use-libravatar";
|
||||
public static final String FORK_IMPORT_THREAD_PROPERTY = "import-with-thread";
|
||||
|
||||
static {
|
||||
configProperties = ProviderConfigurationBuilder.create()
|
||||
.property().name(USE_LIBRAVATAR_PROPERTY)
|
||||
.label("Use Libravater service")
|
||||
.helpText("If the provider not provide the image claim, the email is used to search the avatar "
|
||||
+ "by using libravatar service.")
|
||||
.defaultValue(Boolean.TRUE)
|
||||
.type(ProviderConfigProperty.BOOLEAN_TYPE).add()
|
||||
.property().name(FORK_IMPORT_THREAD_PROPERTY)
|
||||
.label("Import avatar in a separate thread")
|
||||
.helpText("Import the avatar by forking a new thread and don't wait the end of import.")
|
||||
.defaultValue(Boolean.FALSE)
|
||||
.type(ProviderConfigProperty.BOOLEAN_TYPE).add()
|
||||
.build();
|
||||
}
|
||||
|
||||
public static final String MAPPER_ID = "avatar-importer";
|
||||
|
||||
@Override
|
||||
public boolean supportsSyncMode(IdentityProviderSyncMode syncMode) {
|
||||
return IDENTITY_PROVIDER_SYNC_MODES.contains(syncMode);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ProviderConfigProperty> getConfigProperties() {
|
||||
return configProperties;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getId() {
|
||||
return MAPPER_ID;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String[] getCompatibleProviders() {
|
||||
return COMPATIBLE_PROVIDERS;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDisplayCategory() {
|
||||
return "Preprocessor";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDisplayType() {
|
||||
return "Avatar importer";
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateBrokeredUserLegacy(KeycloakSession session, RealmModel realm, UserModel user,
|
||||
IdentityProviderMapperModel mapperModel, BrokeredIdentityContext context) {
|
||||
|
||||
logger.debug("Importing avatar for brokered user legacy");
|
||||
importNewUser(session, realm, user, mapperModel, context);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateBrokeredUser(KeycloakSession session, RealmModel realm, UserModel user,
|
||||
IdentityProviderMapperModel mapperModel, BrokeredIdentityContext context) {
|
||||
|
||||
logger.debug("Importing avatar for brokered user");
|
||||
importNewUser(session, realm, user, mapperModel, context);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void importNewUser(KeycloakSession session, RealmModel realm, UserModel user,
|
||||
IdentityProviderMapperModel mapperModel, BrokeredIdentityContext context) {
|
||||
|
||||
final boolean useLibravatar = Boolean.valueOf(mapperModel.getConfig().get(USE_LIBRAVATAR_PROPERTY));
|
||||
final boolean forkNewThread = Boolean.valueOf(mapperModel.getConfig().get(FORK_IMPORT_THREAD_PROPERTY));
|
||||
|
||||
final String identityProviderAlias = mapperModel.getIdentityProviderAlias();
|
||||
|
||||
Runnable importAvatarRunnable = new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
AvatarStorageProvider avatarStorageProvider = session.getProvider(AvatarStorageProvider.class);
|
||||
if (avatarStorageProvider == null) {
|
||||
logger.warn("Cannot perform avatar import ince the avatar storage provider is null");
|
||||
}
|
||||
InputStream avatarInputStream = null;
|
||||
if (ArrayUtils.contains(AvatarImporter.PROVIDERS_WITH_PICTURE_CLAIM, identityProviderAlias)) {
|
||||
String imageClaim = (String) getClaimValue(context, "picture");
|
||||
if (imageClaim != null && !"".equals(imageClaim)) {
|
||||
logger.infof("Getting avatar from token claim. Value is: %s", imageClaim);
|
||||
try {
|
||||
avatarInputStream = new URL(imageClaim).openStream();
|
||||
} catch (IOException e) {
|
||||
logger.info("Cannot load avatar image from claim: " + imageClaim, e);
|
||||
}
|
||||
}
|
||||
} else if (LinkedInIdentityProviderFactory.PROVIDER_ID.equals(identityProviderAlias)) {
|
||||
avatarInputStream = loadAvatarFromLinkedIn(context);
|
||||
} else if (FacebookIdentityProviderFactory.PROVIDER_ID.equals(identityProviderAlias)) {
|
||||
avatarInputStream = loadAvatarFromFacebook(context);
|
||||
}
|
||||
if (avatarInputStream == null) {
|
||||
if (useLibravatar) {
|
||||
logger.debugf("Trying getting avatar from libravatar service");
|
||||
String email = user.getEmail();
|
||||
try {
|
||||
avatarInputStream = Libravatar.from(email)
|
||||
.withOptions(
|
||||
new LibravatarOptions().withHttps().defaultingTo(LibravatarDefaultImage.NOT_FOUND)
|
||||
.withImageSize(DEFAULT_AVATAR_SIZE))
|
||||
.download();
|
||||
} catch (LibravatarException e) {
|
||||
logger.infof("Avatar not found via libravatar for email: %s", email);
|
||||
}
|
||||
} else {
|
||||
logger.debug("Skipped search on libravatar due to mapper configuration");
|
||||
}
|
||||
}
|
||||
|
||||
if (avatarInputStream != null) {
|
||||
logger.debug("Saving the image via avatar storage provider");
|
||||
avatarStorageProvider.saveAvatarImage(realm, user, avatarInputStream);
|
||||
} else {
|
||||
logger.debugf("No avatar found for user: %s", user);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if (forkNewThread) {
|
||||
logger.debug("Forking new thread to perform the avatar import");
|
||||
new Thread(importAvatarRunnable, "oidc-avatar-import").start();
|
||||
} else {
|
||||
logger.debug("Performing the avatar import in the same thread");
|
||||
importAvatarRunnable.run();
|
||||
}
|
||||
}
|
||||
|
||||
private InputStream loadAvatarFromLinkedIn(BrokeredIdentityContext context) {
|
||||
logger.info("Getting avatar from LinkedIn profile picture prjection");
|
||||
try {
|
||||
JSONObject displayImageProjection = (JSONObject) new JSONParser().parse(new InputStreamReader((new URL(
|
||||
"https://api.linkedin.com/v2/me?projection=(profilePicture(displayImage~:playableStreams))&oauth2_access_token="
|
||||
+ getAccessTokenString(context)).openStream())));
|
||||
|
||||
String imageURL = null;
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
Iterator<JSONObject> elementsIterator = ((JSONArray) ((JSONObject) ((JSONObject) displayImageProjection
|
||||
.get("profilePicture")).get("displayImage~")).get("elements")).iterator();
|
||||
|
||||
while (elementsIterator.hasNext()) {
|
||||
JSONObject element = elementsIterator.next();
|
||||
if ((Double) ((JSONObject) ((JSONObject) ((JSONObject) ((JSONObject) element).get("data"))
|
||||
.get("com.linkedin.digitalmedia.mediaartifact.StillImage")).get("displaySize"))
|
||||
.get("width") == 100.0) {
|
||||
|
||||
imageURL = (String) ((JSONObject) ((JSONArray) ((JSONObject) element).get("identifiers"))
|
||||
.get(0)).get("identifier");
|
||||
}
|
||||
}
|
||||
if (imageURL != null) {
|
||||
logger.infof("Opening stream connnection to %s", imageURL);
|
||||
return new URL(imageURL).openStream();
|
||||
}
|
||||
} catch (IOException | ParseException e) {
|
||||
logger.info("Cannot load LinkedIn avatar image from projection", e);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
protected InputStream loadAvatarFromFacebook(BrokeredIdentityContext context) {
|
||||
logger.info("Getting avatar from Facebook Graph API call");
|
||||
try {
|
||||
return new URL(String.format("https://graph.facebook.com/%s/picture?type=normal", context.getId()))
|
||||
.openStream();
|
||||
} catch (IOException e) {
|
||||
logger.info("Cannot load Facebook avatar image from Graph API", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
protected String getAccessTokenString(BrokeredIdentityContext context) {
|
||||
return (String) context.getContextData().get(KeycloakOIDCIdentityProvider.FEDERATED_ACCESS_TOKEN);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void preprocessFederatedIdentity(KeycloakSession session, RealmModel realm,
|
||||
IdentityProviderMapperModel mapperModel, BrokeredIdentityContext context) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getHelpText() {
|
||||
return "Import the IdP avatar image or use the libravatr service to find it.";
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,46 @@
|
||||
package org.gcube.keycloak.oidc.avatar.importer.libravatar;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.xbill.DNS.Lookup;
|
||||
import org.xbill.DNS.Record;
|
||||
import org.xbill.DNS.SRVRecord;
|
||||
import org.xbill.DNS.TextParseException;
|
||||
import org.xbill.DNS.Type;
|
||||
|
||||
public class DNSUtil {
|
||||
|
||||
protected static final Logger logger = LoggerFactory.getLogger(DNSUtil.class);
|
||||
|
||||
protected final static String PLAIN_DNS_QUERY_TEMPLATE = "_avatars._tcp.%s";
|
||||
|
||||
protected final static String SECURE_DNS_QUERY_TEMPLATE = "_avatars-sec._tcp.%s";
|
||||
|
||||
public static String resolveSRVRecordFromEmail(String email) {
|
||||
return resolveSRVRecordFromEmail(PLAIN_DNS_QUERY_TEMPLATE, email);
|
||||
}
|
||||
|
||||
public static String resolveSecureSRVRecordFromEmail(String email) {
|
||||
return resolveSRVRecordFromEmail(SECURE_DNS_QUERY_TEMPLATE, email);
|
||||
}
|
||||
|
||||
protected static String resolveSRVRecordFromEmail(String queryTemplate, String email) {
|
||||
String[] emailTokens = email.toLowerCase().split("@");
|
||||
String domain = emailTokens[1];
|
||||
logger.trace("Email's domain is: {}", domain);
|
||||
return resolveSRVRecordForDomain(queryTemplate, domain);
|
||||
}
|
||||
|
||||
protected static String resolveSRVRecordForDomain(String queryTemplate, String domain) {
|
||||
String query = String.format(queryTemplate, domain);
|
||||
logger.trace("Performing DNS query for domain: {}", query);
|
||||
try {
|
||||
Record[] records = new Lookup(query, Type.SRV).run();
|
||||
return records != null && records.length > 0 ? ((SRVRecord) records[0]).getTarget().toString() : null;
|
||||
} catch (TextParseException e) {
|
||||
logger.error("Performing the DNS query for SRV: " + query, e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,122 @@
|
||||
package org.gcube.keycloak.oidc.avatar.importer.libravatar;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.net.MalformedURLException;
|
||||
import java.net.URL;
|
||||
import java.util.Map;
|
||||
|
||||
import org.apache.commons.codec.digest.DigestUtils;
|
||||
import org.apache.commons.io.IOUtils;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import com.google.common.base.Preconditions;
|
||||
|
||||
public class Libravatar {
|
||||
|
||||
protected static final Logger logger = LoggerFactory.getLogger(Libravatar.class);
|
||||
|
||||
private String email;
|
||||
private LibravatarOptions options = new LibravatarOptions().withoutHttps();
|
||||
|
||||
private Libravatar(final String email) {
|
||||
Preconditions.checkNotNull(email).toLowerCase();
|
||||
this.email = email;
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct new libravatar downloader for the provided email
|
||||
* @param email the email address
|
||||
* @return the new libravatar downloader instance
|
||||
*/
|
||||
public static Libravatar from(String email) {
|
||||
return new Libravatar(email);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds he options to be used for download
|
||||
* @param options the libravatar options
|
||||
* @return
|
||||
*/
|
||||
public Libravatar withOptions(LibravatarOptions options) {
|
||||
this.options = options;
|
||||
return this;
|
||||
}
|
||||
|
||||
/***
|
||||
* Performs the avatar download by searching for a domain specific server and then on the libravatar public service
|
||||
* @return the avatar's byte array
|
||||
* @throws LibravatarException if no default has been set in options ({@link LibravatarOptions#defaultingTo(LibravatarDefaultImage)}) and the image hasn't found on remote server
|
||||
*/
|
||||
public byte[] downloadArray() throws LibravatarException {
|
||||
try (InputStream is = download()) {
|
||||
return IOUtils.toByteArray(is);
|
||||
} catch (IOException e) {
|
||||
throw new LibravatarException("Cannot download image byte's array for email: " + email, e);
|
||||
}
|
||||
}
|
||||
|
||||
/***
|
||||
* Performs the avatar download by searching for a domain specific server and then on the libravatar public service
|
||||
* @return the avatar byte array
|
||||
* @throws LibravatarException if no default has been set in options ({@link LibravatarOptions#defaultingTo(LibravatarDefaultImage)}) and the image hasn't found on remote server
|
||||
*
|
||||
*/
|
||||
public InputStream download() throws LibravatarException {
|
||||
try {
|
||||
return getURL().openStream();
|
||||
} catch (IOException e) {
|
||||
throw new LibravatarException("Cannot open image stream for email: " + email, e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct the URL to be used for the download
|
||||
* @return the URL to use
|
||||
* @throws MalformedURLException
|
||||
*/
|
||||
private URL getURL() throws MalformedURLException {
|
||||
logger.debug("Constructing download URL for email: {}", email);
|
||||
logger.debug("Getting the DNS queries domain first");
|
||||
String baseTarget = options.isUseHttps() ? DNSUtil.resolveSecureSRVRecordFromEmail(email)
|
||||
: DNSUtil.resolveSRVRecordFromEmail(email);
|
||||
|
||||
if (baseTarget != null) {
|
||||
logger.debug("Building URL with the DNS queried domain");
|
||||
return buildURL(baseTarget);
|
||||
} else {
|
||||
logger.debug("Defaulting to the liravatar URL");
|
||||
return buildURL(options.isUseHttps() ? LibravatarOptions.getSecureBaseUri() : LibravatarOptions.getBaseUri());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the URL with query string to be used starting from the provided base URL
|
||||
* @param baseURL the base url to be used
|
||||
* @return the complete URL ready to be called
|
||||
* @throws MalformedURLException
|
||||
*/
|
||||
private URL buildURL(String baseURL) throws MalformedURLException {
|
||||
StringBuilder sb = new StringBuilder(baseURL);
|
||||
String hash = options.isUseSHA256() ? DigestUtils.sha256Hex(email.getBytes())
|
||||
: DigestUtils.md5Hex(email.getBytes());
|
||||
|
||||
logger.trace("Computed hash is: {}", hash);
|
||||
sb.append(hash);
|
||||
|
||||
Map<String, String> parametersMap = options.toParametersMap();
|
||||
String queryString = parametersMap.entrySet().stream().map(p -> p.getKey() + "=" + p.getValue())
|
||||
.reduce((p1, p2) -> p1 + "&" + p2).orElse("");
|
||||
|
||||
if (queryString != null && !"".equals(queryString)) {
|
||||
sb.append("?");
|
||||
sb.append(queryString);
|
||||
}
|
||||
|
||||
String url = sb.toString();
|
||||
logger.trace("Resulting URL string is: {}", url);
|
||||
return new URL(url);
|
||||
}
|
||||
}
|
@ -0,0 +1,29 @@
|
||||
package org.gcube.keycloak.oidc.avatar.importer.libravatar;
|
||||
|
||||
public enum LibravatarDefaultImage {
|
||||
|
||||
DEFAULT(""),
|
||||
|
||||
MM("mm"),
|
||||
|
||||
IDENTICON("identicon"),
|
||||
|
||||
MONSTERID("monsterid"),
|
||||
|
||||
WAVATAR("wavatar"),
|
||||
|
||||
RETRO("retro"),
|
||||
|
||||
NOT_FOUND("404");
|
||||
|
||||
private final String code;
|
||||
|
||||
private LibravatarDefaultImage(String code) {
|
||||
this.code = code;
|
||||
}
|
||||
|
||||
public String getCode() {
|
||||
return code;
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,32 @@
|
||||
package org.gcube.keycloak.oidc.avatar.importer.libravatar;
|
||||
|
||||
public class LibravatarException extends Exception {
|
||||
|
||||
/**
|
||||
* Serial code version <code>serialVersionUID</code>
|
||||
*/
|
||||
private static final long serialVersionUID = 2574665849051070802L;
|
||||
|
||||
/**
|
||||
* Default constructor
|
||||
*/
|
||||
public LibravatarException() {
|
||||
super();
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param message Exception message text
|
||||
*/
|
||||
public LibravatarException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public LibravatarException(Throwable root) {
|
||||
super(root);
|
||||
}
|
||||
|
||||
public LibravatarException(String message, Throwable root) {
|
||||
super(message, root);
|
||||
}
|
||||
}
|
@ -0,0 +1,151 @@
|
||||
package org.gcube.keycloak.oidc.avatar.importer.libravatar;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import com.google.common.base.Preconditions;
|
||||
|
||||
public class LibravatarOptions {
|
||||
|
||||
/**
|
||||
* Specifies a custom base URI for HTTP use. The default is to use the
|
||||
* official libravatar HTTP server. If you *really* wanted to use a non-free
|
||||
* server, you could set this to "http://gravatar.com/avatar/", but why
|
||||
* would you do such a thing?
|
||||
*/
|
||||
private static String baseUri = "http://cdn.libravatar.org/avatar/";
|
||||
|
||||
/**
|
||||
* Specifies a custom base URI for HTTPS use. The default is to use the
|
||||
* official libravatar HTTPS server.
|
||||
*/
|
||||
private static String secureBaseUri = "https://seccdn.libravatar.org/avatar/";
|
||||
|
||||
/**
|
||||
* Produce https:// URIs where possible. This avoids mixed-content warnings
|
||||
* in browsers when using libravatar-sharp from within a page served via
|
||||
* HTTPS.
|
||||
**/
|
||||
private boolean useHttps;
|
||||
|
||||
/**
|
||||
* Use the SHA256 hash algorithm, rather than MD5. SHA256 is significantly
|
||||
* stronger, but is not supported by Gravatar, so libravatar's fallback to
|
||||
* Gravatar for missing images will not work. Note that using
|
||||
* AvatarUri.FromOpenID implicitly uses SHA256.
|
||||
*/
|
||||
private boolean useSHA256;
|
||||
|
||||
/**
|
||||
* URI for a default image, if no image is found for the user. This also
|
||||
* accepts any of the "special" values in AvatarDefaultImages
|
||||
*/
|
||||
private LibravatarDefaultImage defaultImage;
|
||||
|
||||
/**
|
||||
* Size of the image requested. Valid values are between 1 and 512 pixels.
|
||||
* The default size is 80 pixels.
|
||||
*/
|
||||
private Integer imageSize;
|
||||
|
||||
/**
|
||||
* It is sometimes interesting to test the default option, even for hashes with matching avatars.
|
||||
* This behavior can be triggered by providing the extra parameter <code>forcedefault</code>
|
||||
* or <code>f</code> to the URL with a value of <code>y</code>:
|
||||
*/
|
||||
private boolean forceDefault;
|
||||
|
||||
public LibravatarOptions withHttps() {
|
||||
this.useHttps = true;
|
||||
return this;
|
||||
}
|
||||
|
||||
public LibravatarOptions useSHA256() {
|
||||
this.useSHA256 = true;
|
||||
return this;
|
||||
}
|
||||
|
||||
public LibravatarOptions withImageSize(final Integer imageSize) {
|
||||
Preconditions.checkArgument(imageSize >= 1 && imageSize <= 512);
|
||||
this.imageSize = imageSize;
|
||||
return this;
|
||||
}
|
||||
|
||||
public LibravatarOptions withoutHttps() {
|
||||
this.useHttps = false;
|
||||
return this;
|
||||
}
|
||||
|
||||
public LibravatarOptions defaultingTo(LibravatarDefaultImage defaultImage) {
|
||||
this.defaultImage = defaultImage;
|
||||
return this;
|
||||
}
|
||||
|
||||
public boolean isUseHttps() {
|
||||
return useHttps;
|
||||
}
|
||||
|
||||
public boolean isUseSHA256() {
|
||||
return useSHA256;
|
||||
}
|
||||
|
||||
public Boolean hasDefaultImageSet() {
|
||||
return defaultImage != null;
|
||||
}
|
||||
|
||||
public LibravatarDefaultImage getDefaultImage() {
|
||||
return defaultImage;
|
||||
}
|
||||
|
||||
public Boolean hasImageSizeSet() {
|
||||
return imageSize != null;
|
||||
}
|
||||
|
||||
public Integer getImageSize() {
|
||||
return imageSize;
|
||||
}
|
||||
|
||||
public boolean isForceDefault() {
|
||||
return forceDefault;
|
||||
}
|
||||
|
||||
public Map<String, String> toParametersMap() {
|
||||
return toParametersMap(false);
|
||||
}
|
||||
|
||||
public Map<String, String> toParametersMap(boolean useLongNames) {
|
||||
Map<String, String> parametersMap = new HashMap<>();
|
||||
if (hasImageSizeSet()) {
|
||||
ParameterName p = ParameterName.SIZE;
|
||||
parametersMap.put(useLongNames ? p.getName() : p.getShortName(), String.valueOf(getImageSize()));
|
||||
}
|
||||
|
||||
if (hasDefaultImageSet()) {
|
||||
ParameterName p = ParameterName.DEFAULT;
|
||||
parametersMap.put(useLongNames ? p.getName() : p.getShortName(), getDefaultImage().getCode());
|
||||
}
|
||||
|
||||
if (isForceDefault()) {
|
||||
ParameterName p = ParameterName.FORCE_DEFAULT;
|
||||
parametersMap.put(useLongNames ? p.getName() : p.getShortName(), "y");
|
||||
}
|
||||
return parametersMap;
|
||||
}
|
||||
|
||||
public static void setBaseUri(String baseUri) {
|
||||
LibravatarOptions.baseUri= baseUri;
|
||||
}
|
||||
|
||||
public static String getBaseUri() {
|
||||
return baseUri;
|
||||
}
|
||||
|
||||
public static void setSecureBaseUri(String secureBaseUri) {
|
||||
LibravatarOptions.secureBaseUri= secureBaseUri;
|
||||
}
|
||||
|
||||
public static String getSecureBaseUri() {
|
||||
return secureBaseUri;
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,27 @@
|
||||
package org.gcube.keycloak.oidc.avatar.importer.libravatar;
|
||||
|
||||
public enum ParameterName {
|
||||
|
||||
DEFAULT("default", "d"),
|
||||
|
||||
SIZE("size", "s"),
|
||||
|
||||
FORCE_DEFAULT("forcedefault", "f");
|
||||
|
||||
private final String name;
|
||||
private final String shortName;
|
||||
|
||||
private ParameterName(String name, String shortName) {
|
||||
this.name = name;
|
||||
this.shortName = shortName;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public String getShortName() {
|
||||
return shortName;
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1 @@
|
||||
org.gcube.keycloak.oidc.avatar.importer.AvatarImporter
|
@ -0,0 +1,26 @@
|
||||
package org.gcube.keycloak.avatar.importer.libravatar;
|
||||
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
|
||||
import java.io.InputStream;
|
||||
|
||||
import org.gcube.keycloak.oidc.avatar.importer.libravatar.Libravatar;
|
||||
import org.gcube.keycloak.oidc.avatar.importer.libravatar.LibravatarDefaultImage;
|
||||
import org.gcube.keycloak.oidc.avatar.importer.libravatar.LibravatarException;
|
||||
import org.gcube.keycloak.oidc.avatar.importer.libravatar.LibravatarOptions;
|
||||
import org.junit.Test;
|
||||
|
||||
public class AvatarTest {
|
||||
|
||||
@Test
|
||||
public void testOK() throws LibravatarException {
|
||||
InputStream is = Libravatar.from("mauro.mugnaini@nubisware.com").withOptions(new LibravatarOptions().withHttps()).download();
|
||||
assertNotNull(is);
|
||||
}
|
||||
|
||||
@Test(expected = LibravatarException.class)
|
||||
public void testException() throws LibravatarException {
|
||||
Libravatar.from("fake@superdomainboya.de").withOptions(new LibravatarOptions().withHttps().defaultingTo(LibravatarDefaultImage.NOT_FOUND)).downloadArray();
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,21 @@
|
||||
package org.gcube.keycloak.avatar.importer.libravatar;
|
||||
|
||||
import static org.junit.Assert.assertNull;
|
||||
|
||||
import org.gcube.keycloak.oidc.avatar.importer.libravatar.DNSUtil;
|
||||
import org.junit.Test;
|
||||
|
||||
public class DNSUtilTest {
|
||||
|
||||
@Test
|
||||
public void testKO() {
|
||||
String url = DNSUtil.resolveSecureSRVRecordFromEmail("mauro@fakedomainwithlognnametobesearchedfor.com");
|
||||
assertNull(url);
|
||||
}
|
||||
|
||||
// To be enabled when a domain that expose avatars is found
|
||||
// public void testOK() {
|
||||
// System.out.println(DNSUtil.resolveSecureSRVRecordFromEmail("fake@libravatar.org"));
|
||||
// }
|
||||
|
||||
}
|
@ -0,0 +1,182 @@
|
||||
{
|
||||
"profilePicture": {
|
||||
"displayImage": "urn:li:digitalmediaAsset:C5603AQHMnuXrwSzsgA",
|
||||
"displayImage~": {
|
||||
"paging": {
|
||||
"count": 10,
|
||||
"start": 0,
|
||||
"links": []
|
||||
},
|
||||
"elements": [
|
||||
{
|
||||
"artifact": "urn:li:digitalmediaMediaArtifact:(urn:li:digitalmediaAsset:C5603AQHMnuXrwSzsgA,urn:li:digitalmediaMediaArtifactClass:profile-displayphoto-shrink_100_100)",
|
||||
"authorizationMethod": "PUBLIC",
|
||||
"data": {
|
||||
"com.linkedin.digitalmedia.mediaartifact.StillImage": {
|
||||
"mediaType": "image/jpeg",
|
||||
"rawCodecSpec": {
|
||||
"name": "jpeg",
|
||||
"type": "image"
|
||||
},
|
||||
"displaySize": {
|
||||
"width": 100.0,
|
||||
"uom": "PX",
|
||||
"height": 100.0
|
||||
},
|
||||
"storageSize": {
|
||||
"width": 100,
|
||||
"height": 100
|
||||
},
|
||||
"storageAspectRatio": {
|
||||
"widthAspect": 1.0,
|
||||
"heightAspect": 1.0,
|
||||
"formatted": "1.00:1.00"
|
||||
},
|
||||
"displayAspectRatio": {
|
||||
"widthAspect": 1.0,
|
||||
"heightAspect": 1.0,
|
||||
"formatted": "1.00:1.00"
|
||||
}
|
||||
}
|
||||
},
|
||||
"identifiers": [
|
||||
{
|
||||
"identifier": "https://media-exp1.licdn.com/dms/image/C5603AQHMnuXrwSzsgA/profile-displayphoto-shrink_100_100/0?e=1602720000&v=beta&t=7G3u31o7rJLhxDFtIm8hpBUQkojShy8UvF2Y1JdziNc",
|
||||
"index": 0,
|
||||
"mediaType": "image/jpeg",
|
||||
"file": "urn:li:digitalmediaFile:(urn:li:digitalmediaAsset:C5603AQHMnuXrwSzsgA,urn:li:digitalmediaMediaArtifactClass:profile-displayphoto-shrink_100_100,0)",
|
||||
"identifierType": "EXTERNAL_URL",
|
||||
"identifierExpiresInSeconds": 1602720000
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"artifact": "urn:li:digitalmediaMediaArtifact:(urn:li:digitalmediaAsset:C5603AQHMnuXrwSzsgA,urn:li:digitalmediaMediaArtifactClass:profile-displayphoto-shrink_200_200)",
|
||||
"authorizationMethod": "PUBLIC",
|
||||
"data": {
|
||||
"com.linkedin.digitalmedia.mediaartifact.StillImage": {
|
||||
"mediaType": "image/jpeg",
|
||||
"rawCodecSpec": {
|
||||
"name": "jpeg",
|
||||
"type": "image"
|
||||
},
|
||||
"displaySize": {
|
||||
"width": 200.0,
|
||||
"uom": "PX",
|
||||
"height": 200.0
|
||||
},
|
||||
"storageSize": {
|
||||
"width": 200,
|
||||
"height": 200
|
||||
},
|
||||
"storageAspectRatio": {
|
||||
"widthAspect": 1.0,
|
||||
"heightAspect": 1.0,
|
||||
"formatted": "1.00:1.00"
|
||||
},
|
||||
"displayAspectRatio": {
|
||||
"widthAspect": 1.0,
|
||||
"heightAspect": 1.0,
|
||||
"formatted": "1.00:1.00"
|
||||
}
|
||||
}
|
||||
},
|
||||
"identifiers": [
|
||||
{
|
||||
"identifier": "https://media-exp1.licdn.com/dms/image/C5603AQHMnuXrwSzsgA/profile-displayphoto-shrink_200_200/0?e=1602720000&v=beta&t=jiL5UEJJSeG2Gc_430XJStDrsnro9aAoF-oY9gJEo7Y",
|
||||
"index": 0,
|
||||
"mediaType": "image/jpeg",
|
||||
"file": "urn:li:digitalmediaFile:(urn:li:digitalmediaAsset:C5603AQHMnuXrwSzsgA,urn:li:digitalmediaMediaArtifactClass:profile-displayphoto-shrink_200_200,0)",
|
||||
"identifierType": "EXTERNAL_URL",
|
||||
"identifierExpiresInSeconds": 1602720000
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"artifact": "urn:li:digitalmediaMediaArtifact:(urn:li:digitalmediaAsset:C5603AQHMnuXrwSzsgA,urn:li:digitalmediaMediaArtifactClass:profile-displayphoto-shrink_400_400)",
|
||||
"authorizationMethod": "PUBLIC",
|
||||
"data": {
|
||||
"com.linkedin.digitalmedia.mediaartifact.StillImage": {
|
||||
"mediaType": "image/jpeg",
|
||||
"rawCodecSpec": {
|
||||
"name": "jpeg",
|
||||
"type": "image"
|
||||
},
|
||||
"displaySize": {
|
||||
"width": 400.0,
|
||||
"uom": "PX",
|
||||
"height": 400.0
|
||||
},
|
||||
"storageSize": {
|
||||
"width": 400,
|
||||
"height": 400
|
||||
},
|
||||
"storageAspectRatio": {
|
||||
"widthAspect": 1.0,
|
||||
"heightAspect": 1.0,
|
||||
"formatted": "1.00:1.00"
|
||||
},
|
||||
"displayAspectRatio": {
|
||||
"widthAspect": 1.0,
|
||||
"heightAspect": 1.0,
|
||||
"formatted": "1.00:1.00"
|
||||
}
|
||||
}
|
||||
},
|
||||
"identifiers": [
|
||||
{
|
||||
"identifier": "https://media-exp1.licdn.com/dms/image/C5603AQHMnuXrwSzsgA/profile-displayphoto-shrink_400_400/0?e=1602720000&v=beta&t=q_j_fFbr2-V5Fa2-ROQ6r9NmBC6SALGLVos19b2aGXk",
|
||||
"index": 0,
|
||||
"mediaType": "image/jpeg",
|
||||
"file": "urn:li:digitalmediaFile:(urn:li:digitalmediaAsset:C5603AQHMnuXrwSzsgA,urn:li:digitalmediaMediaArtifactClass:profile-displayphoto-shrink_400_400,0)",
|
||||
"identifierType": "EXTERNAL_URL",
|
||||
"identifierExpiresInSeconds": 1602720000
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"artifact": "urn:li:digitalmediaMediaArtifact:(urn:li:digitalmediaAsset:C5603AQHMnuXrwSzsgA,urn:li:digitalmediaMediaArtifactClass:profile-displayphoto-shrink_800_800)",
|
||||
"authorizationMethod": "PUBLIC",
|
||||
"data": {
|
||||
"com.linkedin.digitalmedia.mediaartifact.StillImage": {
|
||||
"mediaType": "image/jpeg",
|
||||
"rawCodecSpec": {
|
||||
"name": "jpeg",
|
||||
"type": "image"
|
||||
},
|
||||
"displaySize": {
|
||||
"width": 800.0,
|
||||
"uom": "PX",
|
||||
"height": 800.0
|
||||
},
|
||||
"storageSize": {
|
||||
"width": 800,
|
||||
"height": 800
|
||||
},
|
||||
"storageAspectRatio": {
|
||||
"widthAspect": 1.0,
|
||||
"heightAspect": 1.0,
|
||||
"formatted": "1.00:1.00"
|
||||
},
|
||||
"displayAspectRatio": {
|
||||
"widthAspect": 1.0,
|
||||
"heightAspect": 1.0,
|
||||
"formatted": "1.00:1.00"
|
||||
}
|
||||
}
|
||||
},
|
||||
"identifiers": [
|
||||
{
|
||||
"identifier": "https://media-exp1.licdn.com/dms/image/C5603AQHMnuXrwSzsgA/profile-displayphoto-shrink_800_800/0?e=1602720000&v=beta&t=eFX1HoYUCOp3NcHD_GkA4QBGlLK_-0sIl-6-DdmXk08",
|
||||
"index": 0,
|
||||
"mediaType": "image/jpeg",
|
||||
"file": "urn:li:digitalmediaFile:(urn:li:digitalmediaAsset:C5603AQHMnuXrwSzsgA,urn:li:digitalmediaMediaArtifactClass:profile-displayphoto-shrink_800_800,0)",
|
||||
"identifierType": "EXTERNAL_URL",
|
||||
"identifierExpiresInSeconds": 1602720000
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,24 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<log4j:configuration
|
||||
xmlns="http://jakarta.apache.org/log4j/"
|
||||
xmlns:log4j="http://jakarta.apache.org/log4j/">
|
||||
|
||||
<appender name="console"
|
||||
class="org.apache.log4j.ConsoleAppender">
|
||||
<layout class="org.apache.log4j.PatternLayout">
|
||||
<param name="ConversionPattern"
|
||||
value="%d{yyyy-MM-dd HH:mm:ss} %-5p %c{1}:%L - %m%n" />
|
||||
</layout>
|
||||
</appender>
|
||||
|
||||
<logger name="org.gcube" additivity="false">
|
||||
<level value="TRACE" />
|
||||
<appender-ref ref="console" />
|
||||
</logger>
|
||||
|
||||
<root>
|
||||
<level value="INFO" />
|
||||
<appender-ref ref="console" />
|
||||
</root>
|
||||
|
||||
</log4j:configuration>
|
@ -0,0 +1,7 @@
|
||||
This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
# Changelog for "avatar-realm-resource"
|
||||
|
||||
## [v0.2.0-SNAPSHOT]
|
||||
- First released version. Provides avatar resource, exposes via REST interface, can be configured in the UI with proper interface. It also provide two implementation for the storage: one file system based and one user's attribute based (to be tested with an external the DB implementation, it works well with H2 default DB).
|
||||
|
@ -0,0 +1,26 @@
|
||||
# Acknowledgments
|
||||
|
||||
The projects leading to this software have received funding from a series of European Union programmes including:
|
||||
|
||||
- the Sixth Framework Programme for Research and Technological Development
|
||||
- [DILIGENT](https://cordis.europa.eu/project/id/004260) (grant no. 004260).
|
||||
- the Seventh Framework Programme for research, technological development and demonstration
|
||||
- [D4Science](https://cordis.europa.eu/project/id/212488) (grant no. 212488);
|
||||
- [D4Science-II](https://cordis.europa.eu/project/id/239019) (grant no.239019);
|
||||
- [ENVRI](https://cordis.europa.eu/project/id/283465) (grant no. 283465);
|
||||
- [iMarine](https://cordis.europa.eu/project/id/283644) (grant no. 283644);
|
||||
- [EUBrazilOpenBio](https://cordis.europa.eu/project/id/288754) (grant no. 288754).
|
||||
- the H2020 research and innovation programme
|
||||
- [SoBigData](https://cordis.europa.eu/project/id/654024) (grant no. 654024);
|
||||
- [PARTHENOS](https://cordis.europa.eu/project/id/654119) (grant no. 654119);
|
||||
- [EGI-Engage](https://cordis.europa.eu/project/id/654142) (grant no. 654142);
|
||||
- [ENVRI PLUS](https://cordis.europa.eu/project/id/654182) (grant no. 654182);
|
||||
- [BlueBRIDGE](https://cordis.europa.eu/project/id/675680) (grant no. 675680);
|
||||
- [PerformFISH](https://cordis.europa.eu/project/id/727610) (grant no. 727610);
|
||||
- [AGINFRA PLUS](https://cordis.europa.eu/project/id/731001) (grant no. 731001);
|
||||
- [DESIRA](https://cordis.europa.eu/project/id/818194) (grant no. 818194);
|
||||
- [ARIADNEplus](https://cordis.europa.eu/project/id/823914) (grant no. 823914);
|
||||
- [RISIS 2](https://cordis.europa.eu/project/id/824091) (grant no. 824091);
|
||||
- [EOSC-Pillar](https://cordis.europa.eu/project/id/857650) (grant no. 857650);
|
||||
- [Blue Cloud](https://cordis.europa.eu/project/id/862409) (grant no. 862409);
|
||||
- [SoBigData-PlusPlus](https://cordis.europa.eu/project/id/871042) (grant no. 871042);
|
@ -0,0 +1,46 @@
|
||||
# Event Publisher Portal
|
||||
|
||||
**Keycloak Realm Resource** defines the new avatar resource inside Keycloak and exposes it on REST, implements the SPI defined in the `avatar-storage` module to store avatar on file system or in an user's property (to be deeply tested on all the persistence since it could be limited by the JDBC driver/JPA implementation.
|
||||
|
||||
## Structure of the project
|
||||
|
||||
The source code is present in `src` folder.
|
||||
|
||||
## Built With
|
||||
|
||||
* [OpenJDK](https://openjdk.java.net/) - The JDK used
|
||||
* [Maven](https://maven.apache.org/) - Dependency Management
|
||||
|
||||
## Documentation
|
||||
|
||||
This is one of the modules that composes the EAR deployment defined in the "brother" module [keycloak-d4science-spi](../keycloak-d4science-spi-ear/README.md).
|
||||
|
||||
To build the JAR artifact it is sufficient to type
|
||||
|
||||
mvn clean package
|
||||
|
||||
## Change log
|
||||
|
||||
See [CHANGELOG.md](CHANGELOG.md).
|
||||
|
||||
## Authors
|
||||
|
||||
* **Mauro Mugnaini** ([Nubisware S.r.l.](http://www.nubisware.com))
|
||||
|
||||
## How to Cite this Software
|
||||
[Intentionally left blank]
|
||||
|
||||
## License
|
||||
|
||||
This project is licensed under the EUPL V.1.1 License - see the [LICENSE.md](LICENSE.md) file for details.
|
||||
|
||||
## About the gCube Framework
|
||||
This software is part of the [gCubeFramework](https://www.gcube-system.org/ "gCubeFramework"): an
|
||||
open-source software toolkit used for building and operating Hybrid Data
|
||||
Infrastructures enabling the dynamic deployment of Virtual Research Environments
|
||||
by favouring the realisation of reuse oriented policies.
|
||||
|
||||
The projects leading to this software have received funding from a series of European Union programmes see [FUNDING.md](FUNDING.md)
|
||||
|
||||
## Acknowledgments
|
||||
[Intentionally left blank]
|
@ -0,0 +1,67 @@
|
||||
package org.gcube.keycloak.avatar;
|
||||
|
||||
import java.io.InputStream;
|
||||
|
||||
import javax.ws.rs.core.Response;
|
||||
import javax.ws.rs.core.StreamingOutput;
|
||||
|
||||
import org.apache.commons.io.IOUtils;
|
||||
import org.gcube.keycloak.avatar.storage.AvatarStorageProvider;
|
||||
import org.gcube.keycloak.avatar.storage.file.FileAvatarStorageProvider;
|
||||
import org.jboss.logging.Logger;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.UserModel;
|
||||
|
||||
public abstract class AbstractAvatarResource {
|
||||
|
||||
protected static final Logger logger = Logger.getLogger(AbstractAvatarResource.class);
|
||||
|
||||
protected static final String AVATAR_IMAGE_PARAMETER = "image";
|
||||
|
||||
public static final Class<?> DEFAULT_IMPLEMENTATION = FileAvatarStorageProvider.class;
|
||||
|
||||
protected KeycloakSession session;
|
||||
|
||||
public AbstractAvatarResource(KeycloakSession session) {
|
||||
this.session = session;
|
||||
}
|
||||
|
||||
public AvatarStorageProvider getAvatarStorageProvider() {
|
||||
AvatarStorageProvider asp = lookupAvatarStorageProvider(session);
|
||||
if (asp == null) {
|
||||
logger.warnf("Provider not found via SPI configuration, defualting to: %s",
|
||||
DEFAULT_IMPLEMENTATION.getName());
|
||||
|
||||
try {
|
||||
asp = (AvatarStorageProvider) DEFAULT_IMPLEMENTATION.newInstance();
|
||||
} catch (InstantiationException | IllegalAccessException e) {
|
||||
logger.error("Cannot instatiate storage implementation class", e);
|
||||
}
|
||||
}
|
||||
return asp;
|
||||
}
|
||||
|
||||
protected AvatarStorageProvider lookupAvatarStorageProvider(KeycloakSession keycloakSession) {
|
||||
return keycloakSession.getProvider(AvatarStorageProvider.class);
|
||||
}
|
||||
|
||||
protected Response unauthorized() {
|
||||
return Response.status(Response.Status.UNAUTHORIZED).build();
|
||||
}
|
||||
|
||||
protected Response invalidState() {
|
||||
return Response.status(Response.Status.FORBIDDEN).build();
|
||||
}
|
||||
|
||||
protected void saveUserImage(RealmModel realm, UserModel user, InputStream imageInputStream) {
|
||||
getAvatarStorageProvider().saveAvatarImage(realm, user, imageInputStream);
|
||||
}
|
||||
|
||||
protected StreamingOutput fetchUserImage(RealmModel realm, UserModel user) {
|
||||
AvatarStorageProvider asp = getAvatarStorageProvider();
|
||||
InputStream is = asp.loadAvatarImage(realm, user);
|
||||
return output -> IOUtils.copy(is, output);
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,177 @@
|
||||
package org.gcube.keycloak.avatar;
|
||||
|
||||
import java.io.InputStream;
|
||||
|
||||
import javax.ws.rs.Consumes;
|
||||
import javax.ws.rs.ForbiddenException;
|
||||
import javax.ws.rs.GET;
|
||||
import javax.ws.rs.NotAuthorizedException;
|
||||
import javax.ws.rs.NotFoundException;
|
||||
import javax.ws.rs.POST;
|
||||
import javax.ws.rs.Path;
|
||||
import javax.ws.rs.PathParam;
|
||||
import javax.ws.rs.Produces;
|
||||
import javax.ws.rs.core.Context;
|
||||
import javax.ws.rs.core.HttpHeaders;
|
||||
import javax.ws.rs.core.MediaType;
|
||||
import javax.ws.rs.core.MultivaluedMap;
|
||||
import javax.ws.rs.core.Response;
|
||||
|
||||
import org.gcube.keycloak.avatar.storage.AvatarStorageProvider;
|
||||
import org.jboss.resteasy.annotations.cache.NoCache;
|
||||
import org.jboss.resteasy.plugins.providers.multipart.MultipartFormDataInput;
|
||||
import org.keycloak.common.ClientConnection;
|
||||
import org.keycloak.jose.jws.JWSInput;
|
||||
import org.keycloak.jose.jws.JWSInputException;
|
||||
import org.keycloak.models.ClientModel;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.UserModel;
|
||||
import org.keycloak.representations.AccessToken;
|
||||
import org.keycloak.services.managers.AppAuthManager;
|
||||
import org.keycloak.services.managers.AuthenticationManager;
|
||||
import org.keycloak.services.managers.RealmManager;
|
||||
import org.keycloak.services.resources.admin.AdminAuth;
|
||||
import org.keycloak.services.resources.admin.permissions.AdminPermissionEvaluator;
|
||||
import org.keycloak.services.resources.admin.permissions.AdminPermissions;
|
||||
|
||||
public class AvatarAdminResource extends AbstractAvatarResource {
|
||||
|
||||
private AdminPermissionEvaluator realmAuth;
|
||||
|
||||
@Context
|
||||
private AvatarStorageProvider avatarStorageProvider;
|
||||
|
||||
private AppAuthManager authManager;
|
||||
// private TokenManager tokenManager;
|
||||
|
||||
@Context
|
||||
private HttpHeaders httpHeaders;
|
||||
|
||||
@Context
|
||||
private ClientConnection clientConnection;
|
||||
|
||||
private AdminAuth auth;
|
||||
|
||||
public AvatarAdminResource(KeycloakSession session) {
|
||||
super(session);
|
||||
authManager = new AppAuthManager();
|
||||
// tokenManager = new TokenManager();
|
||||
}
|
||||
|
||||
public void init() {
|
||||
RealmModel realm = session.getContext().getRealm();
|
||||
|
||||
auth = authenticateRealmAdminRequest();
|
||||
|
||||
RealmManager realmManager = new RealmManager(session);
|
||||
if (realm == null)
|
||||
throw new NotFoundException("Realm not found");
|
||||
|
||||
if (!auth.getRealm().equals(realmManager.getKeycloakAdminstrationRealm()) && !auth.getRealm().equals(realm)) {
|
||||
throw new org.keycloak.services.ForbiddenException();
|
||||
}
|
||||
realmAuth = AdminPermissions.evaluator(session, realm, auth);
|
||||
|
||||
session.getContext().setRealm(realm);
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("/{user_id}")
|
||||
@Produces({ "image/png", "image/jpeg", "image/gif" })
|
||||
public Response downloadUserAvatarImage(@PathParam("user_id") String userId) {
|
||||
try {
|
||||
canViewUsers();
|
||||
UserModel user = session.users().getUserById(userId, session.getContext().getRealm());
|
||||
return Response.ok(fetchUserImage(session.getContext().getRealm(), user)).build();
|
||||
|
||||
} catch (ForbiddenException e) {
|
||||
return Response.status(Response.Status.FORBIDDEN).entity(e.getMessage()).build();
|
||||
} catch (Exception e) {
|
||||
logger.error("error getting user avatar", e);
|
||||
return Response.serverError().entity(e.getMessage()).build();
|
||||
}
|
||||
}
|
||||
|
||||
@POST
|
||||
@NoCache
|
||||
@Path("/{user_id}")
|
||||
@Consumes(MediaType.MULTIPART_FORM_DATA)
|
||||
public Response uploadUserAvatarImage(@PathParam("user_id") String userId, MultipartFormDataInput input) {
|
||||
try {
|
||||
if (auth == null) {
|
||||
return Response.status(Response.Status.UNAUTHORIZED).build();
|
||||
}
|
||||
canManageUsers();
|
||||
|
||||
RealmModel realm = session.getContext().getRealm();
|
||||
UserModel user = session.users().getUserById(userId, session.getContext().getRealm());
|
||||
|
||||
InputStream imageInputStream = input.getFormDataPart(AVATAR_IMAGE_PARAMETER, InputStream.class, null);
|
||||
saveUserImage(realm, user, imageInputStream);
|
||||
} catch (ForbiddenException e) {
|
||||
return Response.status(Response.Status.FORBIDDEN).entity(e.getMessage()).build();
|
||||
} catch (Exception e) {
|
||||
logger.error("error saving user avatar", e);
|
||||
return Response.serverError().entity(e.getMessage()).build();
|
||||
}
|
||||
|
||||
return Response.ok().build();
|
||||
}
|
||||
|
||||
protected AdminAuth authenticateRealmAdminRequest() {
|
||||
String tokenString = authManager.extractAuthorizationHeaderToken(httpHeaders);
|
||||
MultivaluedMap<String, String> queryParameters = session.getContext().getUri().getQueryParameters();
|
||||
if (tokenString == null && queryParameters.containsKey("access_token")) {
|
||||
tokenString = queryParameters.getFirst("access_token");
|
||||
}
|
||||
if (tokenString == null) {
|
||||
throw new NotAuthorizedException("Bearer");
|
||||
}
|
||||
AccessToken token;
|
||||
try {
|
||||
JWSInput input = new JWSInput(tokenString);
|
||||
token = input.readJsonContent(AccessToken.class);
|
||||
} catch (JWSInputException e) {
|
||||
throw new NotAuthorizedException("Bearer token format error");
|
||||
}
|
||||
String realmName = token.getIssuer().substring(token.getIssuer().lastIndexOf('/') + 1);
|
||||
RealmManager realmManager = new RealmManager(session);
|
||||
RealmModel realm = realmManager.getRealmByName(realmName);
|
||||
if (realm == null) {
|
||||
throw new NotAuthorizedException("Unknown realm in token");
|
||||
}
|
||||
session.getContext().setRealm(realm);
|
||||
AuthenticationManager.AuthResult authResult = authManager.authenticateBearerToken(tokenString, session, realm,
|
||||
session.getContext().getUri(), clientConnection, httpHeaders);
|
||||
|
||||
if (authResult == null) {
|
||||
logger.debug("Token not valid");
|
||||
throw new NotAuthorizedException("Bearer");
|
||||
}
|
||||
|
||||
ClientModel client = realm.getClientByClientId(token.getIssuedFor());
|
||||
if (client == null) {
|
||||
throw new NotFoundException("Could not find client for authorization");
|
||||
|
||||
}
|
||||
|
||||
return new AdminAuth(realm, authResult.getToken(), authResult.getUser(), client);
|
||||
}
|
||||
|
||||
private void canViewUsers() {
|
||||
if (!realmAuth.users().canView()) {
|
||||
String message = "user does not have permission to view users";
|
||||
logger.info(message);
|
||||
throw new ForbiddenException(message);
|
||||
}
|
||||
}
|
||||
|
||||
private void canManageUsers() {
|
||||
if (!realmAuth.users().canManage()) {
|
||||
String message = "user does not have permission to manage users";
|
||||
logger.info(message);
|
||||
throw new ForbiddenException(message);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,102 @@
|
||||
package org.gcube.keycloak.avatar;
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.util.Objects;
|
||||
|
||||
import javax.ws.rs.Consumes;
|
||||
import javax.ws.rs.GET;
|
||||
import javax.ws.rs.POST;
|
||||
import javax.ws.rs.Path;
|
||||
import javax.ws.rs.Produces;
|
||||
import javax.ws.rs.core.Context;
|
||||
import javax.ws.rs.core.MediaType;
|
||||
import javax.ws.rs.core.Response;
|
||||
import javax.ws.rs.core.UriInfo;
|
||||
|
||||
import org.jboss.resteasy.annotations.cache.NoCache;
|
||||
import org.jboss.resteasy.plugins.providers.multipart.MultipartFormDataInput;
|
||||
import org.jboss.resteasy.spi.ResteasyProviderFactory;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.services.managers.AppAuthManager;
|
||||
import org.keycloak.services.managers.AuthenticationManager;
|
||||
import org.keycloak.services.resources.RealmsResource;
|
||||
|
||||
public class AvatarResource extends AbstractAvatarResource {
|
||||
|
||||
public static final String STATE_CHECKER_ATTRIBUTE = "state_checker";
|
||||
public static final String STATE_CHECKER_PARAMETER = "stateChecker";
|
||||
|
||||
private final AuthenticationManager.AuthResult auth;
|
||||
|
||||
public AvatarResource(KeycloakSession session) {
|
||||
super(session);
|
||||
this.auth = resolveAuthentication(session);
|
||||
}
|
||||
|
||||
private AuthenticationManager.AuthResult resolveAuthentication(KeycloakSession keycloakSession) {
|
||||
AppAuthManager appAuthManager = new AppAuthManager();
|
||||
RealmModel realm = keycloakSession.getContext().getRealm();
|
||||
|
||||
return appAuthManager.authenticateIdentityCookie(keycloakSession, realm);
|
||||
}
|
||||
|
||||
@Path("/admin")
|
||||
public AvatarAdminResource admin() {
|
||||
AvatarAdminResource service = new AvatarAdminResource(session);
|
||||
ResteasyProviderFactory.getInstance().injectProperties(service);
|
||||
service.init();
|
||||
return service;
|
||||
}
|
||||
|
||||
@GET
|
||||
@Produces({ "image/png", "image/jpeg", "image/gif" })
|
||||
public Response downloadCurrentUserAvatarImage() {
|
||||
if (auth == null) {
|
||||
return unauthorized();
|
||||
}
|
||||
return Response.ok(fetchUserImage(auth.getSession().getRealm(), auth.getUser())).build();
|
||||
}
|
||||
|
||||
@POST
|
||||
@NoCache
|
||||
@Consumes(MediaType.MULTIPART_FORM_DATA)
|
||||
public Response uploadCurrentUserAvatarImage(MultipartFormDataInput input, @Context UriInfo uriInfo) {
|
||||
if (auth == null) {
|
||||
return unauthorized();
|
||||
}
|
||||
|
||||
if (!isValidStateChecker(input)) {
|
||||
return invalidState();
|
||||
}
|
||||
|
||||
try {
|
||||
InputStream imageInputStream = input.getFormDataPart(AVATAR_IMAGE_PARAMETER, InputStream.class, null);
|
||||
|
||||
saveUserImage(auth.getSession().getRealm(), auth.getUser(), imageInputStream);
|
||||
|
||||
if (uriInfo.getQueryParameters().containsKey("account")) {
|
||||
return Response.seeOther(
|
||||
RealmsResource.accountUrl(session.getContext().getUri().getBaseUriBuilder())
|
||||
.build(auth.getSession().getRealm().getName())).build();
|
||||
}
|
||||
|
||||
return Response.ok().build();
|
||||
|
||||
} catch (Exception ex) {
|
||||
return Response.serverError().build();
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isValidStateChecker(MultipartFormDataInput input) {
|
||||
try {
|
||||
String actualStateChecker = input.getFormDataPart(STATE_CHECKER_PARAMETER, String.class, null);
|
||||
String requiredStateChecker = (String) session.getAttribute(STATE_CHECKER_ATTRIBUTE);
|
||||
|
||||
return Objects.equals(requiredStateChecker, actualStateChecker);
|
||||
} catch (Exception ex) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,30 @@
|
||||
package org.gcube.keycloak.avatar;
|
||||
|
||||
import org.jboss.logging.Logger;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.services.resource.RealmResourceProvider;
|
||||
|
||||
public class AvatarResourceProvider implements RealmResourceProvider {
|
||||
|
||||
private static final Logger logger = Logger.getLogger(AvatarResourceProvider.class);
|
||||
|
||||
private final KeycloakSession keycloakSession;
|
||||
|
||||
public AvatarResourceProvider(KeycloakSession keycloakSession) {
|
||||
logger.debugf("New avatar resource provider created for session: %s",
|
||||
keycloakSession.getContext().getContextPath());
|
||||
|
||||
this.keycloakSession = keycloakSession;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getResource() {
|
||||
logger.trace("Getting new avatar resource");
|
||||
return new AvatarResource(keycloakSession);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
logger.trace("Closing avatar resource provider");
|
||||
}
|
||||
}
|
@ -0,0 +1,47 @@
|
||||
package org.gcube.keycloak.avatar;
|
||||
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.KeycloakSessionFactory;
|
||||
import org.keycloak.services.resource.RealmResourceProvider;
|
||||
import org.keycloak.services.resource.RealmResourceProviderFactory;
|
||||
|
||||
import static org.keycloak.Config.Scope;
|
||||
|
||||
import org.jboss.logging.Logger;
|
||||
|
||||
public class AvatarResourceProviderFactory implements RealmResourceProviderFactory {
|
||||
|
||||
private static final Logger logger = Logger.getLogger(AvatarResourceProviderFactory.class);
|
||||
|
||||
private AvatarResourceProvider avatarResourceProvider;
|
||||
|
||||
public AvatarResourceProviderFactory() {
|
||||
logger.debug("Creating new avatar resource provider factory");
|
||||
}
|
||||
|
||||
@Override
|
||||
public RealmResourceProvider create(KeycloakSession keycloakSession) {
|
||||
if (avatarResourceProvider == null) {
|
||||
avatarResourceProvider = new AvatarResourceProvider(keycloakSession);
|
||||
}
|
||||
return avatarResourceProvider;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void init(Scope scope) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void postInit(KeycloakSessionFactory keycloakSessionFactory) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
// NOOP
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getId() {
|
||||
return "avatar-provider";
|
||||
}
|
||||
}
|
@ -0,0 +1,86 @@
|
||||
package org.gcube.keycloak.avatar.storage.file;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
|
||||
import org.apache.commons.io.IOUtils;
|
||||
import org.gcube.keycloak.avatar.storage.AvatarStorageProvider;
|
||||
import org.jboss.logging.Logger;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.UserModel;
|
||||
|
||||
public class FileAvatarStorageProvider implements AvatarStorageProvider {
|
||||
|
||||
private static final Logger logger = Logger.getLogger(FileAvatarStorageProvider.class);
|
||||
|
||||
private final File avatarFolder;
|
||||
private final Boolean useEmailAsFilename;
|
||||
|
||||
public FileAvatarStorageProvider() {
|
||||
this(FileAvatarStorageProviderFactory.DEFAULT_AVATAR_FOLDER,
|
||||
FileAvatarStorageProviderFactory.USE_EMAIL_AS_FILENAME_DEFAULT);
|
||||
}
|
||||
|
||||
public FileAvatarStorageProvider(String avatarFolder, Boolean useEmailAsFilename) {
|
||||
this.avatarFolder = new File(avatarFolder);
|
||||
this.useEmailAsFilename = useEmailAsFilename;
|
||||
if (!this.avatarFolder.exists()) {
|
||||
logger.infof("Creating avatar folder: %s", this.avatarFolder.getAbsolutePath());
|
||||
this.avatarFolder.mkdir();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void saveAvatarImage(RealmModel realm, UserModel user, InputStream input) {
|
||||
checkRealmFolderExistence(realm.getName());
|
||||
File avatarFile = getAvatarFile(realm, user);
|
||||
if (!avatarFile.exists()) {
|
||||
try {
|
||||
avatarFile.createNewFile();
|
||||
} catch (IOException e) {
|
||||
logger.error("Cannot create new avater file", e);
|
||||
}
|
||||
}
|
||||
try (FileOutputStream fos = new FileOutputStream(avatarFile)) {
|
||||
IOUtils.copy(input, fos);
|
||||
} catch (IOException e) {
|
||||
logger.error("Cannot save avatar stream into file", e);
|
||||
}
|
||||
}
|
||||
|
||||
private void checkRealmFolderExistence(String realmName) {
|
||||
File realmFile = new File(avatarFolder, realmName);
|
||||
if (!realmFile.exists()) {
|
||||
logger.infof("Creating avatar folder for realm '%s' in %s", realmName, realmFile.getAbsolutePath());
|
||||
realmFile.mkdir();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public InputStream loadAvatarImage(RealmModel realm, UserModel user) {
|
||||
checkRealmFolderExistence(realm.getName());
|
||||
File avatarFile = getAvatarFile(realm, user);
|
||||
try {
|
||||
return new FileInputStream(avatarFile);
|
||||
} catch (FileNotFoundException e) {
|
||||
logger.debugf("Avatar file not found for user '%s' in realm '%s'", user.getUsername(), realm.getName());
|
||||
return new ByteArrayInputStream(new byte[0]);
|
||||
}
|
||||
}
|
||||
|
||||
private File getAvatarFile(RealmModel realm, UserModel user) {
|
||||
return new File(new File(avatarFolder, realm.getName()),
|
||||
useEmailAsFilename ? user.getEmail() : user.getUsername());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
// NOOP
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,43 @@
|
||||
package org.gcube.keycloak.avatar.storage.file;
|
||||
|
||||
import org.gcube.keycloak.avatar.storage.AvatarStorageProvider;
|
||||
import org.gcube.keycloak.avatar.storage.AvatarStorageProviderFactory;
|
||||
import org.keycloak.Config;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.KeycloakSessionFactory;
|
||||
|
||||
public class FileAvatarStorageProviderFactory implements AvatarStorageProviderFactory {
|
||||
|
||||
public static final String DEFAULT_AVATAR_FOLDER = System.getProperty("jboss.server.data.dir") + "/avatar";
|
||||
public static final Boolean USE_EMAIL_AS_FILENAME_DEFAULT = Boolean.FALSE;
|
||||
|
||||
private String avatarFolder;
|
||||
private Boolean useEmailAsFilename;
|
||||
|
||||
@Override
|
||||
public AvatarStorageProvider create(KeycloakSession session) {
|
||||
return new FileAvatarStorageProvider(avatarFolder, useEmailAsFilename);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void init(Config.Scope config) {
|
||||
avatarFolder = config.get("avatar-folder", DEFAULT_AVATAR_FOLDER);
|
||||
useEmailAsFilename = config.getBoolean("use-email-as-filename", USE_EMAIL_AS_FILENAME_DEFAULT);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void postInit(KeycloakSessionFactory factory) {
|
||||
// NOOP
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
// NOOP
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getId() {
|
||||
return "avatar-storage-file";
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,61 @@
|
||||
package org.gcube.keycloak.avatar.storage.userattribute;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.Base64;
|
||||
|
||||
import org.apache.commons.io.IOUtils;
|
||||
import org.gcube.keycloak.avatar.storage.AvatarStorageProvider;
|
||||
import org.jboss.logging.Logger;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.UserModel;
|
||||
|
||||
public class UserAttributeAvatarStorageProvider implements AvatarStorageProvider {
|
||||
|
||||
private static final Logger logger = Logger.getLogger(UserAttributeAvatarStorageProvider.class);
|
||||
|
||||
private final String userAttribute;
|
||||
|
||||
public UserAttributeAvatarStorageProvider() {
|
||||
this(UserAttributeAvatarStorageProviderFactory.USER_ATTRIBUTE_NAME);
|
||||
}
|
||||
|
||||
public UserAttributeAvatarStorageProvider(String userAttribute) {
|
||||
this.userAttribute = userAttribute;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void saveAvatarImage(RealmModel realm, UserModel user, InputStream input) {
|
||||
logger.debugf("Saving avatar image to user attribute: %s", userAttribute);
|
||||
user.setSingleAttribute(userAttribute, getBase64EncodedString(input));
|
||||
}
|
||||
|
||||
private String getBase64EncodedString(InputStream input) {
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||
try {
|
||||
IOUtils.copy(input, baos);
|
||||
} catch (IOException e) {
|
||||
logger.error("Getting image stream as bytes array", e);
|
||||
}
|
||||
return Base64.getEncoder().encodeToString(baos.toByteArray());
|
||||
}
|
||||
|
||||
@Override
|
||||
public InputStream loadAvatarImage(RealmModel realm, UserModel user) {
|
||||
logger.debugf("Getting avatar image from user attribute: %s", userAttribute);
|
||||
return toBinaryStream(user.getFirstAttribute(userAttribute));
|
||||
}
|
||||
|
||||
private InputStream toBinaryStream(String base64EncodedImage) {
|
||||
byte[] decoded = base64EncodedImage != null ? Base64.getDecoder().decode(base64EncodedImage) : new byte[0];
|
||||
return new ByteArrayInputStream(decoded);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
// NOOP
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,38 @@
|
||||
package org.gcube.keycloak.avatar.storage.userattribute;
|
||||
|
||||
import org.gcube.keycloak.avatar.storage.AvatarStorageProvider;
|
||||
import org.gcube.keycloak.avatar.storage.AvatarStorageProviderFactory;
|
||||
import org.keycloak.Config;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.KeycloakSessionFactory;
|
||||
|
||||
public class UserAttributeAvatarStorageProviderFactory implements AvatarStorageProviderFactory {
|
||||
|
||||
public static final String USER_ATTRIBUTE_NAME = "avatar";
|
||||
|
||||
@Override
|
||||
public AvatarStorageProvider create(KeycloakSession session) {
|
||||
return new UserAttributeAvatarStorageProvider(USER_ATTRIBUTE_NAME);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void init(Config.Scope config) {
|
||||
// NOOP
|
||||
}
|
||||
|
||||
@Override
|
||||
public void postInit(KeycloakSessionFactory factory) {
|
||||
// NOOP
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
// NOOP
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getId() {
|
||||
return "avatar-storage-user_attribute";
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,11 @@
|
||||
{
|
||||
"themes": [
|
||||
{
|
||||
"name": "account-avatar",
|
||||
"types": [
|
||||
"account",
|
||||
"admin"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
@ -0,0 +1 @@
|
||||
org.gcube.keycloak.avatar.storage.file.FileAvatarStorageProviderFactory
|
@ -0,0 +1 @@
|
||||
org.gcube.keycloak.avatar.AvatarResourceProviderFactory
|
@ -0,0 +1,93 @@
|
||||
<#import "template.ftl" as layout>
|
||||
<@layout.mainLayout active='account' bodyClass='user'; section>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-10">
|
||||
<h2>${msg("editAccountHtmlTitle")}</h2>
|
||||
</div>
|
||||
<div class="col-md-2 subtitle">
|
||||
<span class="subtitle"><span class="required">*</span> ${msg("requiredFields")}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form action="${url.accountUrl}" class="form-horizontal" method="post">
|
||||
|
||||
<input type="hidden" id="stateChecker" name="stateChecker" value="${stateChecker}">
|
||||
|
||||
<#if !realm.registrationEmailAsUsername>
|
||||
<div class="form-group ${messagesPerField.printIfExists('username','has-error')}">
|
||||
<div class="col-sm-2 col-md-2">
|
||||
<label for="username" class="control-label">${msg("username")}</label> <#if realm.editUsernameAllowed><span class="required">*</span></#if>
|
||||
</div>
|
||||
|
||||
<div class="col-sm-10 col-md-10">
|
||||
<input type="text" class="form-control" id="username" name="username" <#if !realm.editUsernameAllowed>disabled="disabled"</#if> value="${(account.username!'')}"/>
|
||||
</div>
|
||||
</div>
|
||||
</#if>
|
||||
|
||||
<div class="form-group ${messagesPerField.printIfExists('email','has-error')}">
|
||||
<div class="col-sm-2 col-md-2">
|
||||
<label for="email" class="control-label">${msg("email")}</label> <span class="required">*</span>
|
||||
</div>
|
||||
|
||||
<div class="col-sm-10 col-md-10">
|
||||
<input type="text" class="form-control" id="email" name="email" autofocus value="${(account.email!'')}"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group ${messagesPerField.printIfExists('firstName','has-error')}">
|
||||
<div class="col-sm-2 col-md-2">
|
||||
<label for="firstName" class="control-label">${msg("firstName")}</label> <span class="required">*</span>
|
||||
</div>
|
||||
|
||||
<div class="col-sm-10 col-md-10">
|
||||
<input type="text" class="form-control" id="firstName" name="firstName" value="${(account.firstName!'')}"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group ${messagesPerField.printIfExists('lastName','has-error')}">
|
||||
<div class="col-sm-2 col-md-2">
|
||||
<label for="lastName" class="control-label">${msg("lastName")}</label> <span class="required">*</span>
|
||||
</div>
|
||||
|
||||
<div class="col-sm-10 col-md-10">
|
||||
<input type="text" class="form-control" id="lastName" name="lastName" value="${(account.lastName!'')}"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<div id="kc-form-buttons" class="col-md-offset-2 col-md-10 submit">
|
||||
<div class="">
|
||||
<#if url.referrerURI??><a href="${url.referrerURI}">${kcSanitize(msg("backToApplication")?no_esc)}</a></#if>
|
||||
<button type="submit" class="${properties.kcButtonClass!} ${properties.kcButtonPrimaryClass!} ${properties.kcButtonLargeClass!}" name="submitAction" value="Save">${msg("doSave")}</button>
|
||||
<button type="submit" class="${properties.kcButtonClass!} ${properties.kcButtonDefaultClass!} ${properties.kcButtonLargeClass!}" name="submitAction" value="Cancel">${msg("doCancel")}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-10">
|
||||
<h2>${msg("changeAvatarHtmlTitle")}</h2>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<#assign avatarUrl = url.accountUrl?replace("^(.*)(/account/?)(\\?(.*))?$", "$1/avatar-provider/?account&$4", 'r') />
|
||||
<form action="${avatarUrl}" class="form-horizontal" method="post" enctype="multipart/form-data">
|
||||
|
||||
<img src="${avatarUrl}" style="max-width:200px;" >
|
||||
<input type="file" id="avatar" name="image">
|
||||
|
||||
<input type="hidden" name="stateChecker" value="${stateChecker}">
|
||||
|
||||
<div class="form-group">
|
||||
<div id="kc-form-buttons" class="col-md-offset-2 col-md-10 submit">
|
||||
<div class="">
|
||||
<button type="submit" class="${properties.kcButtonClass!} ${properties.kcButtonPrimaryClass!} ${properties.kcButtonLargeClass!}" name="submitAction" value="Save">${msg("doSave")}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
</@layout.mainLayout>
|
@ -0,0 +1 @@
|
||||
changeAvatarHtmlTitle=Profilbild bearbeiten
|
@ -0,0 +1 @@
|
||||
changeAvatarHtmlTitle=Edit Avatar
|
@ -0,0 +1 @@
|
||||
parent=keycloak
|
@ -0,0 +1,26 @@
|
||||
module.service('UserAvatar', function(Auth) {
|
||||
this.url = function(user, realm) {
|
||||
return authUrl + '/realms/' + realm.realm + '/avatar-provider/admin/' + user.id + "?access_token=" + Auth.authz.token + "&" + + new Date().getTime();
|
||||
}
|
||||
});
|
||||
|
||||
module.controller('UserAvatarCtrl', function($scope, $http, Notifications, UserAvatar) {
|
||||
$scope.avatarUrl = UserAvatar.url($scope.user, $scope.realm);
|
||||
|
||||
$scope.uploadAvatar = function(files) {
|
||||
var fd = new FormData();
|
||||
//Take the first selected file
|
||||
fd.append("image", files[0]);
|
||||
|
||||
$http.post($scope.avatarUrl, fd, {
|
||||
headers: {'Content-Type': undefined },
|
||||
transformRequest: angular.identity
|
||||
}).then(function() {
|
||||
Notifications.success("Your changes have been saved to the user.");
|
||||
$scope.avatarUrl = UserAvatar.url($scope.user, $scope.realm);
|
||||
}, function(error) {
|
||||
console.error(error);
|
||||
Notifications.error("Could not save the avatar");
|
||||
});
|
||||
}
|
||||
});
|
@ -0,0 +1,168 @@
|
||||
<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}}/users">{{:: 'users' | translate}}</a></li>
|
||||
<li data-ng-hide="create">{{user.username}}</li>
|
||||
<li data-ng-show="create">{{:: 'add-user' | translate}}</li>
|
||||
</ol>
|
||||
|
||||
<kc-tabs-user></kc-tabs-user>
|
||||
|
||||
<form class="form-horizontal" name="userForm" novalidate kc-read-only="!create && !user.access.manage">
|
||||
|
||||
<fieldset class="border-top">
|
||||
<div class="form-group">
|
||||
<label class="col-md-2 control-label"for="id">{{:: 'id' | translate}}</label>
|
||||
<div class="col-md-6">
|
||||
<input class="form-control" type="text" id="id" name="id" data-ng-model="user.id" autofocus data-ng-readonly="true">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="col-md-2 control-label"for="id">{{:: 'created-at' | translate}}</label>
|
||||
<div class="col-md-6">
|
||||
{{user.createdTimestamp|date:'shortDate'}} {{user.createdTimestamp|date:'mediumTime'}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="col-md-2 control-label"for="username">{{:: 'username' | translate}} <span class="required" data-ng-show="create">*</span></label>
|
||||
<div class="col-md-6">
|
||||
<!-- Characters >,<,/,\ are forbidden in username -->
|
||||
<input class="form-control" type="text" id="username" name="username" data-ng-model="user.username" autofocus
|
||||
required ng-pattern="/^[^\<\>\\\/]*$/" data-ng-readonly="!editUsername">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="form-group">
|
||||
<label class="col-md-2 control-label" for="email">{{:: 'email' | translate}}</label>
|
||||
|
||||
<div class="col-md-6">
|
||||
<input class="form-control" type="email" name="email" id="email"
|
||||
data-ng-model="user.email">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="col-md-2 control-label" for="firstName">{{:: 'first-name' | translate}}</label>
|
||||
|
||||
<div class="col-md-6">
|
||||
<input class="form-control" type="text" name="firstName" id="firstName"
|
||||
data-ng-model="user.firstName">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="col-md-2 control-label" for="lastName">{{:: 'last-name' | translate}}</label>
|
||||
|
||||
<div class="col-md-6">
|
||||
<input class="form-control" type="text" name="lastName" id="lastName"
|
||||
data-ng-model="user.lastName">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group clearfix block">
|
||||
<label class="col-md-2 control-label" for="userEnabled">{{:: 'user-enabled' | translate}}</label>
|
||||
<div class="col-md-6">
|
||||
<input ng-model="user.enabled" name="userEnabled" id="userEnabled" ng-disabled="!create && !user.access.manage" onoffswitch on-text="{{:: 'onText' | translate}}" off-text="{{:: 'offText' | translate}}"/>
|
||||
</div>
|
||||
<kc-tooltip>{{:: 'user-enabled.tooltip' | translate}}</kc-tooltip>
|
||||
</div>
|
||||
<div class="form-group clearfix block" data-ng-show="realm.bruteForceProtected && !create">
|
||||
<label class="col-md-2 control-label" for="temporarilyDisabled">{{:: 'user-temporarily-locked' | translate}}</label>
|
||||
<div class="col-md-1">
|
||||
<input ng-model="temporarilyDisabled" name="temporarilyDisabled" id="temporarilyDisabled" data-ng-readonly="true" data-ng-disabled="true" onoffswitch on-text="{{:: 'onText' | translate}}" off-text="{{:: 'offText' | translate}}"/>
|
||||
</div>
|
||||
<kc-tooltip>{{:: 'user-temporarily-locked.tooltip' | translate}}</kc-tooltip>
|
||||
<div class="col-sm-2">
|
||||
<button type="submit" data-ng-click="unlockUser()" data-ng-show="temporarilyDisabled" class="btn btn-default">{{:: 'unlock-user' | translate}}</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group clearfix block" data-ng-show="!create && user.federationLink">
|
||||
<label class="col-md-2 control-label">{{:: 'federation-link' | translate}}</label>
|
||||
<div class="col-md-6">
|
||||
<a href="{{federationLink}}">{{federationLinkName}}</a>
|
||||
</div>
|
||||
<kc-tooltip>{{:: 'user-link.tooltip' | translate}}</kc-tooltip>
|
||||
</div>
|
||||
<div class="form-group clearfix block" data-ng-show="!create && user.origin">
|
||||
<label class="col-md-2 control-label">{{:: 'user-origin-link' | translate}}</label>
|
||||
<div class="col-md-6">
|
||||
<a href="{{originLink}}">{{originName}}</a>
|
||||
</div>
|
||||
<kc-tooltip>{{:: 'user-origin.tooltip' | translate}}</kc-tooltip>
|
||||
</div>
|
||||
<div class="form-group clearfix block">
|
||||
<label class="col-md-2 control-label" for="emailVerified">{{:: 'email-verified' | translate}}</label>
|
||||
<div class="col-md-6">
|
||||
<input ng-model="user.emailVerified" name="emailVerified" id="emailVerified" ng-disabled="!create && !user.access.manage" onoffswitch on-text="{{:: 'onText' | translate}}" off-text="{{:: 'offText' | translate}}"/>
|
||||
</div>
|
||||
<kc-tooltip>{{:: 'email-verified.tooltip' | translate}}</kc-tooltip>
|
||||
</div>
|
||||
<div class="form-group clearfix">
|
||||
<label class="col-md-2 control-label" for="reqActions">{{:: 'required-user-actions' | translate}}</label>
|
||||
|
||||
<div class="col-md-6">
|
||||
<select ui-select2 id="reqActions" ng-model="user.requiredActions" data-placeholder="{{:: 'select-an-action.placeholder' | translate}}" multiple>
|
||||
<option ng-repeat="action in userReqActionList" value="{{action.alias}}">{{action.name}}</option>
|
||||
</select>
|
||||
</div>
|
||||
<kc-tooltip>{{:: 'required-user-actions.tooltip' | translate}}</kc-tooltip>
|
||||
</div>
|
||||
|
||||
<div class="form-group clearfix" data-ng-if="realm.internationalizationEnabled">
|
||||
<label class="col-md-2 control-label" for="locale">{{:: 'locale' | translate}}</label>
|
||||
<div class="col-md-6">
|
||||
<div>
|
||||
<select class="form-control" id="locale"
|
||||
ng-model="user.attributes.locale"
|
||||
ng-options="o as o for o in realm.supportedLocales">
|
||||
<option value="" disabled selected>{{:: 'select-one.placeholder' | translate}}</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group clearfix" data-ng-hide="create || !access.impersonation">
|
||||
<label class="col-md-2 control-label" for="impersonate">{{:: 'impersonate-user' | translate}}</label>
|
||||
|
||||
<div class="col-md-6">
|
||||
<button id="impersonate" data-ng-show="access.impersonation" kc-read-only-ignore class="btn btn-default" data-ng-click="impersonate()">{{:: 'impersonate' | translate}}</button>
|
||||
</div>
|
||||
<kc-tooltip>{{:: 'impersonate-user.tooltip' | translate}}</kc-tooltip>
|
||||
</div>
|
||||
|
||||
</fieldset>
|
||||
|
||||
<div class="form-group">
|
||||
<div class="col-md-10 col-md-offset-2" data-ng-show="create && access.manageUsers">
|
||||
<button kc-save data-ng-show="changed">{{:: 'save' | translate}}</button>
|
||||
<button kc-cancel data-ng-click="cancel()">{{:: 'cancel' | translate}}</button>
|
||||
</div>
|
||||
|
||||
<div class="col-md-10 col-md-offset-2" data-ng-show="!create && user.access.manage">
|
||||
<button kc-save data-ng-disabled="!changed">{{:: 'save' | translate}}</button>
|
||||
<button kc-reset data-ng-disabled="!changed">{{:: 'cancel' | translate}}</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</form>
|
||||
<form class="form-horizontal" ng-controller="UserAvatarCtrl" novalidate>
|
||||
<fieldset class="border-top">
|
||||
<legend><span class="text">Avatar</span></legend>
|
||||
<div class="form-group">
|
||||
<div class="col-md-10 col-md-offset-2">
|
||||
<img style="max-width:600px;" src="{{ avatarUrl }}" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="col-md-2 control-label" for="image">Upload</label>
|
||||
|
||||
<div class="col-md-6">
|
||||
<input class="form-control" type="file" name="image" id="image" onchange="angular.element(this).scope().uploadAvatar(this.files)" />
|
||||
</div>
|
||||
</div>
|
||||
</fieldset>
|
||||
</form>
|
||||
|
||||
</div>
|
||||
|
||||
<kc-menu></kc-menu>
|
@ -0,0 +1,2 @@
|
||||
parent=keycloak
|
||||
scripts=js/user-avatar.js
|
@ -0,0 +1,6 @@
|
||||
This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
# Changelog for "avatar-storage"
|
||||
|
||||
## [v0.2.0-SNAPSHOT]
|
||||
- First release, it defines the avatar storage SPI
|
@ -0,0 +1,26 @@
|
||||
# Acknowledgments
|
||||
|
||||
The projects leading to this software have received funding from a series of European Union programmes including:
|
||||
|
||||
- the Sixth Framework Programme for Research and Technological Development
|
||||
- [DILIGENT](https://cordis.europa.eu/project/id/004260) (grant no. 004260).
|
||||
- the Seventh Framework Programme for research, technological development and demonstration
|
||||
- [D4Science](https://cordis.europa.eu/project/id/212488) (grant no. 212488);
|
||||
- [D4Science-II](https://cordis.europa.eu/project/id/239019) (grant no.239019);
|
||||
- [ENVRI](https://cordis.europa.eu/project/id/283465) (grant no. 283465);
|
||||
- [iMarine](https://cordis.europa.eu/project/id/283644) (grant no. 283644);
|
||||
- [EUBrazilOpenBio](https://cordis.europa.eu/project/id/288754) (grant no. 288754).
|
||||
- the H2020 research and innovation programme
|
||||
- [SoBigData](https://cordis.europa.eu/project/id/654024) (grant no. 654024);
|
||||
- [PARTHENOS](https://cordis.europa.eu/project/id/654119) (grant no. 654119);
|
||||
- [EGI-Engage](https://cordis.europa.eu/project/id/654142) (grant no. 654142);
|
||||
- [ENVRI PLUS](https://cordis.europa.eu/project/id/654182) (grant no. 654182);
|
||||
- [BlueBRIDGE](https://cordis.europa.eu/project/id/675680) (grant no. 675680);
|
||||
- [PerformFISH](https://cordis.europa.eu/project/id/727610) (grant no. 727610);
|
||||
- [AGINFRA PLUS](https://cordis.europa.eu/project/id/731001) (grant no. 731001);
|
||||
- [DESIRA](https://cordis.europa.eu/project/id/818194) (grant no. 818194);
|
||||
- [ARIADNEplus](https://cordis.europa.eu/project/id/823914) (grant no. 823914);
|
||||
- [RISIS 2](https://cordis.europa.eu/project/id/824091) (grant no. 824091);
|
||||
- [EOSC-Pillar](https://cordis.europa.eu/project/id/857650) (grant no. 857650);
|
||||
- [Blue Cloud](https://cordis.europa.eu/project/id/862409) (grant no. 862409);
|
||||
- [SoBigData-PlusPlus](https://cordis.europa.eu/project/id/871042) (grant no. 871042);
|
@ -0,0 +1,58 @@
|
||||
# Event Publisher Portal
|
||||
|
||||
**Avatar Storage** defines a new [Keycloak](https://www.keycloak.org)'s SPI to plug avatar persistence strategy via `services` definition.
|
||||
|
||||
## Structure of the project
|
||||
|
||||
The source code is present in `src` folder.
|
||||
|
||||
## Built With
|
||||
|
||||
* [OpenJDK](https://openjdk.java.net/) - The JDK used
|
||||
* [Maven](https://maven.apache.org/) - Dependency Management
|
||||
|
||||
## Documentation
|
||||
|
||||
This is one of the modules that composes the EAR deployment defined in the "brother" module [keycloak-d4science-spi](../keycloak-d4science-spi-ear/README.md).
|
||||
|
||||
To build the module JAR file it is sufficient to type
|
||||
|
||||
mvn clean package
|
||||
|
||||
The module can be installed inside the locally running Keycloak runtime (when the Keycloak server is stopped) by using the shell file:
|
||||
|
||||
install-keycloak-module.sh
|
||||
|
||||
Then, after the module has been installed and the server has been started, you can enable it with:
|
||||
|
||||
add-avatar-resource-provider.sh [host:port]
|
||||
|
||||
This will make the new defined SPI available in the Keycloak server. The `[host:port]` parameter is optional and defaults to Wildfly control default host and port (`localhost:9990`)
|
||||
|
||||
(NOTE: Both commands are using the `$KEYCLOAK_HOME` environment variable to find where Keycloak server is located)
|
||||
|
||||
## Change log
|
||||
|
||||
See [CHANGELOG.md](CHANGELOG.md).
|
||||
|
||||
## Authors
|
||||
|
||||
* **Mauro Mugnaini** ([Nubisware S.r.l.](http://www.nubisware.com))
|
||||
|
||||
## How to Cite this Software
|
||||
[Intentionally left blank]
|
||||
|
||||
## License
|
||||
|
||||
This project is licensed under the EUPL V.1.1 License - see the [LICENSE.md](LICENSE.md) file for details.
|
||||
|
||||
## About the gCube Framework
|
||||
This software is part of the [gCubeFramework](https://www.gcube-system.org/ "gCubeFramework"): an
|
||||
open-source software toolkit used for building and operating Hybrid Data
|
||||
Infrastructures enabling the dynamic deployment of Virtual Research Environments
|
||||
by favouring the realisation of reuse oriented policies.
|
||||
|
||||
The projects leading to this software have received funding from a series of European Union programmes see [FUNDING.md](FUNDING.md)
|
||||
|
||||
## Acknowledgments
|
||||
[Intentionally left blank]
|
@ -0,0 +1 @@
|
||||
$KEYCLOAK_HOME/bin/jboss-cli.sh --connect --controller=${1-localhost:9990} --command="/subsystem=keycloak-server:list-add(name=providers, value=module:org.gcube.keycloak.avatar-storage)"
|
@ -0,0 +1 @@
|
||||
$KEYCLOAK_HOME/bin/jboss-cli.sh --command="module add --name=org.gcube.keycloak.avatar-storage --resources=target/avatar-storage-0.1.0-SNAPSHOT.jar --dependencies=org.keycloak.keycloak-core,org.keycloak.keycloak-services,org.keycloak.keycloak-server-spi,org.keycloak.keycloak-server-spi-private,org.jboss.logging"
|
@ -0,0 +1,18 @@
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<parent>
|
||||
<groupId>org.gcube</groupId>
|
||||
<artifactId>keycloak-d4science-spi-parent</artifactId>
|
||||
<version>0.1.0-SNAPSHOT</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>avatar-storage</artifactId>
|
||||
<packaging>jar</packaging>
|
||||
|
||||
<build />
|
||||
|
||||
</project>
|
@ -0,0 +1,15 @@
|
||||
package org.gcube.keycloak.avatar.storage;
|
||||
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.UserModel;
|
||||
import org.keycloak.provider.Provider;
|
||||
|
||||
import java.io.InputStream;
|
||||
|
||||
public interface AvatarStorageProvider extends Provider {
|
||||
|
||||
void saveAvatarImage(RealmModel realm, UserModel user, InputStream input);
|
||||
|
||||
InputStream loadAvatarImage(RealmModel realm, UserModel user);
|
||||
|
||||
}
|
@ -0,0 +1,7 @@
|
||||
package org.gcube.keycloak.avatar.storage;
|
||||
|
||||
import org.keycloak.provider.ProviderFactory;
|
||||
|
||||
public interface AvatarStorageProviderFactory extends ProviderFactory<AvatarStorageProvider> {
|
||||
|
||||
}
|
@ -0,0 +1,36 @@
|
||||
package org.gcube.keycloak.avatar.storage;
|
||||
|
||||
import org.jboss.logging.Logger;
|
||||
import org.keycloak.provider.Provider;
|
||||
import org.keycloak.provider.ProviderFactory;
|
||||
import org.keycloak.provider.Spi;
|
||||
|
||||
public class AvatarStorageProviderSpi implements Spi {
|
||||
|
||||
private static final Logger logger = Logger.getLogger(AvatarStorageProviderSpi.class);
|
||||
|
||||
public AvatarStorageProviderSpi() {
|
||||
logger.debug("Creating new avatar storage provider SPI");
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isInternal() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "avatar-storage";
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<? extends Provider> getProviderClass() {
|
||||
return AvatarStorageProvider.class;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<? extends ProviderFactory<AvatarStorageProvider>> getProviderFactoryClass() {
|
||||
return AvatarStorageProviderFactory.class;
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1 @@
|
||||
org.gcube.keycloak.avatar.storage.AvatarStorageProviderSpi
|
@ -0,0 +1,6 @@
|
||||
This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
# Changelog for "event-listener-provider"
|
||||
|
||||
## [v0.2.0-SNAPSHOT]
|
||||
- Extracted sub-module from the original project. Send Keycloak's events to an orchestrator endpoint
|
@ -0,0 +1,26 @@
|
||||
# Acknowledgments
|
||||
|
||||
The projects leading to this software have received funding from a series of European Union programmes including:
|
||||
|
||||
- the Sixth Framework Programme for Research and Technological Development
|
||||
- [DILIGENT](https://cordis.europa.eu/project/id/004260) (grant no. 004260).
|
||||
- the Seventh Framework Programme for research, technological development and demonstration
|
||||
- [D4Science](https://cordis.europa.eu/project/id/212488) (grant no. 212488);
|
||||
- [D4Science-II](https://cordis.europa.eu/project/id/239019) (grant no.239019);
|
||||
- [ENVRI](https://cordis.europa.eu/project/id/283465) (grant no. 283465);
|
||||
- [iMarine](https://cordis.europa.eu/project/id/283644) (grant no. 283644);
|
||||
- [EUBrazilOpenBio](https://cordis.europa.eu/project/id/288754) (grant no. 288754).
|
||||
- the H2020 research and innovation programme
|
||||
- [SoBigData](https://cordis.europa.eu/project/id/654024) (grant no. 654024);
|
||||
- [PARTHENOS](https://cordis.europa.eu/project/id/654119) (grant no. 654119);
|
||||
- [EGI-Engage](https://cordis.europa.eu/project/id/654142) (grant no. 654142);
|
||||
- [ENVRI PLUS](https://cordis.europa.eu/project/id/654182) (grant no. 654182);
|
||||
- [BlueBRIDGE](https://cordis.europa.eu/project/id/675680) (grant no. 675680);
|
||||
- [PerformFISH](https://cordis.europa.eu/project/id/727610) (grant no. 727610);
|
||||
- [AGINFRA PLUS](https://cordis.europa.eu/project/id/731001) (grant no. 731001);
|
||||
- [DESIRA](https://cordis.europa.eu/project/id/818194) (grant no. 818194);
|
||||
- [ARIADNEplus](https://cordis.europa.eu/project/id/823914) (grant no. 823914);
|
||||
- [RISIS 2](https://cordis.europa.eu/project/id/824091) (grant no. 824091);
|
||||
- [EOSC-Pillar](https://cordis.europa.eu/project/id/857650) (grant no. 857650);
|
||||
- [Blue Cloud](https://cordis.europa.eu/project/id/862409) (grant no. 862409);
|
||||
- [SoBigData-PlusPlus](https://cordis.europa.eu/project/id/871042) (grant no. 871042);
|
@ -0,0 +1,47 @@
|
||||
# Event Publisher Portal
|
||||
|
||||
**Event Listener Provider** extends the [Keycloak](https://www.keycloak.org)'s event SPI to push JSON events to an orchestrator endpoint.
|
||||
|
||||
## Structure of the project
|
||||
|
||||
The source code is present in `src` folder.
|
||||
|
||||
## Built With
|
||||
|
||||
* [OpenJDK](https://openjdk.java.net/) - The JDK used
|
||||
* [Maven](https://maven.apache.org/) - Dependency Management
|
||||
|
||||
## Documentation
|
||||
|
||||
This is one of the modules that composes the EAR deployment defined in the "brother" module [keycloak-d4science-spi](../keycloak-d4science-spi-ear/README.md).
|
||||
|
||||
To build the JAR file it is sufficient to type
|
||||
|
||||
mvn clean package
|
||||
|
||||
## Change log
|
||||
|
||||
See [CHANGELOG.md](CHANGELOG.md).
|
||||
|
||||
## Authors
|
||||
|
||||
* **Marco Lettere** ([Nubisware S.r.l.](http://www.nubisware.com))
|
||||
* **Mauro Mugnaini** ([Nubisware S.r.l.](http://www.nubisware.com))
|
||||
|
||||
## How to Cite this Software
|
||||
[Intentionally left blank]
|
||||
|
||||
## License
|
||||
|
||||
This project is licensed under the EUPL V.1.1 License - see the [LICENSE.md](LICENSE.md) file for details.
|
||||
|
||||
## About the gCube Framework
|
||||
This software is part of the [gCubeFramework](https://www.gcube-system.org/ "gCubeFramework"): an
|
||||
open-source software toolkit used for building and operating Hybrid Data
|
||||
Infrastructures enabling the dynamic deployment of Virtual Research Environments
|
||||
by favouring the realisation of reuse oriented policies.
|
||||
|
||||
The projects leading to this software have received funding from a series of European Union programmes see [FUNDING.md](FUNDING.md)
|
||||
|
||||
## Acknowledgments
|
||||
[Intentionally left blank]
|
@ -0,0 +1,6 @@
|
||||
This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
# Changelog for "identity-provider-mapper"
|
||||
|
||||
## [v0.2.0-SNAPSHOT]
|
||||
- Extracted sub-module from the original project. Extends the identity provider mapper SPI to extract the username for the users that use and IdP to subscribe itself to the d4science portal.
|
@ -0,0 +1,26 @@
|
||||
# Acknowledgments
|
||||
|
||||
The projects leading to this software have received funding from a series of European Union programmes including:
|
||||
|
||||
- the Sixth Framework Programme for Research and Technological Development
|
||||
- [DILIGENT](https://cordis.europa.eu/project/id/004260) (grant no. 004260).
|
||||
- the Seventh Framework Programme for research, technological development and demonstration
|
||||
- [D4Science](https://cordis.europa.eu/project/id/212488) (grant no. 212488);
|
||||
- [D4Science-II](https://cordis.europa.eu/project/id/239019) (grant no.239019);
|
||||
- [ENVRI](https://cordis.europa.eu/project/id/283465) (grant no. 283465);
|
||||
- [iMarine](https://cordis.europa.eu/project/id/283644) (grant no. 283644);
|
||||
- [EUBrazilOpenBio](https://cordis.europa.eu/project/id/288754) (grant no. 288754).
|
||||
- the H2020 research and innovation programme
|
||||
- [SoBigData](https://cordis.europa.eu/project/id/654024) (grant no. 654024);
|
||||
- [PARTHENOS](https://cordis.europa.eu/project/id/654119) (grant no. 654119);
|
||||
- [EGI-Engage](https://cordis.europa.eu/project/id/654142) (grant no. 654142);
|
||||
- [ENVRI PLUS](https://cordis.europa.eu/project/id/654182) (grant no. 654182);
|
||||
- [BlueBRIDGE](https://cordis.europa.eu/project/id/675680) (grant no. 675680);
|
||||
- [PerformFISH](https://cordis.europa.eu/project/id/727610) (grant no. 727610);
|
||||
- [AGINFRA PLUS](https://cordis.europa.eu/project/id/731001) (grant no. 731001);
|
||||
- [DESIRA](https://cordis.europa.eu/project/id/818194) (grant no. 818194);
|
||||
- [ARIADNEplus](https://cordis.europa.eu/project/id/823914) (grant no. 823914);
|
||||
- [RISIS 2](https://cordis.europa.eu/project/id/824091) (grant no. 824091);
|
||||
- [EOSC-Pillar](https://cordis.europa.eu/project/id/857650) (grant no. 857650);
|
||||
- [Blue Cloud](https://cordis.europa.eu/project/id/862409) (grant no. 862409);
|
||||
- [SoBigData-PlusPlus](https://cordis.europa.eu/project/id/871042) (grant no. 871042);
|
@ -0,0 +1,46 @@
|
||||
# Event Publisher Portal
|
||||
|
||||
**Identity Provider Mapper** extends the [Keycloak](https://www.keycloak.org)'s identity provider mapper SPI to extract the username from the email address.
|
||||
|
||||
## Structure of the project
|
||||
|
||||
The source code is present in `src` folder.
|
||||
|
||||
## Built With
|
||||
|
||||
* [OpenJDK](https://openjdk.java.net/) - The JDK used
|
||||
* [Maven](https://maven.apache.org/) - Dependency Management
|
||||
|
||||
## Documentation
|
||||
|
||||
This is one of the modules that composes the EAR deployment defined in the "brother" module [keycloak-d4science-spi](../keycloak-d4science-spi-ear/README.md).
|
||||
|
||||
To build the JAR artifact it is sufficient to type
|
||||
|
||||
mvn clean package
|
||||
|
||||
## Change log
|
||||
|
||||
See [CHANGELOG.md](CHANGELOG.md).
|
||||
|
||||
## Authors
|
||||
|
||||
* **Mauro Mugnaini** ([Nubisware S.r.l.](http://www.nubisware.com))
|
||||
|
||||
## How to Cite this Software
|
||||
[Intentionally left blank]
|
||||
|
||||
## License
|
||||
|
||||
This project is licensed under the EUPL V.1.1 License - see the [LICENSE.md](LICENSE.md) file for details.
|
||||
|
||||
## About the gCube Framework
|
||||
This software is part of the [gCubeFramework](https://www.gcube-system.org/ "gCubeFramework"): an
|
||||
open-source software toolkit used for building and operating Hybrid Data
|
||||
Infrastructures enabling the dynamic deployment of Virtual Research Environments
|
||||
by favouring the realisation of reuse oriented policies.
|
||||
|
||||
The projects leading to this software have received funding from a series of European Union programmes see [FUNDING.md](FUNDING.md)
|
||||
|
||||
## Acknowledgments
|
||||
[Intentionally left blank]
|
@ -0,0 +1,6 @@
|
||||
This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
# Changelog for "keycloak-extension-spi"
|
||||
|
||||
## [v0.2.0-SNAPSHOT]
|
||||
- Extracted sub-module from the original project. It bundle all the modules in one J2EE EAR ready to be deployed
|
@ -0,0 +1,26 @@
|
||||
# Acknowledgments
|
||||
|
||||
The projects leading to this software have received funding from a series of European Union programmes including:
|
||||
|
||||
- the Sixth Framework Programme for Research and Technological Development
|
||||
- [DILIGENT](https://cordis.europa.eu/project/id/004260) (grant no. 004260).
|
||||
- the Seventh Framework Programme for research, technological development and demonstration
|
||||
- [D4Science](https://cordis.europa.eu/project/id/212488) (grant no. 212488);
|
||||
- [D4Science-II](https://cordis.europa.eu/project/id/239019) (grant no.239019);
|
||||
- [ENVRI](https://cordis.europa.eu/project/id/283465) (grant no. 283465);
|
||||
- [iMarine](https://cordis.europa.eu/project/id/283644) (grant no. 283644);
|
||||
- [EUBrazilOpenBio](https://cordis.europa.eu/project/id/288754) (grant no. 288754).
|
||||
- the H2020 research and innovation programme
|
||||
- [SoBigData](https://cordis.europa.eu/project/id/654024) (grant no. 654024);
|
||||
- [PARTHENOS](https://cordis.europa.eu/project/id/654119) (grant no. 654119);
|
||||
- [EGI-Engage](https://cordis.europa.eu/project/id/654142) (grant no. 654142);
|
||||
- [ENVRI PLUS](https://cordis.europa.eu/project/id/654182) (grant no. 654182);
|
||||
- [BlueBRIDGE](https://cordis.europa.eu/project/id/675680) (grant no. 675680);
|
||||
- [PerformFISH](https://cordis.europa.eu/project/id/727610) (grant no. 727610);
|
||||
- [AGINFRA PLUS](https://cordis.europa.eu/project/id/731001) (grant no. 731001);
|
||||
- [DESIRA](https://cordis.europa.eu/project/id/818194) (grant no. 818194);
|
||||
- [ARIADNEplus](https://cordis.europa.eu/project/id/823914) (grant no. 823914);
|
||||
- [RISIS 2](https://cordis.europa.eu/project/id/824091) (grant no. 824091);
|
||||
- [EOSC-Pillar](https://cordis.europa.eu/project/id/857650) (grant no. 857650);
|
||||
- [Blue Cloud](https://cordis.europa.eu/project/id/862409) (grant no. 862409);
|
||||
- [SoBigData-PlusPlus](https://cordis.europa.eu/project/id/871042) (grant no. 871042);
|
@ -0,0 +1,44 @@
|
||||
# Event Publisher Portal
|
||||
|
||||
**Keycloak D4Science Bundle** assemble all the modules in one J2EE EAR ready to be deployed inside a Keycloak installation.
|
||||
|
||||
## Structure of the project
|
||||
|
||||
The source of the EAR configuration is located under `src/main/application` folder.
|
||||
|
||||
## Built With
|
||||
|
||||
* [OpenJDK](https://openjdk.java.net/) - The JDK used
|
||||
* [Maven](https://maven.apache.org/) - Dependency Management
|
||||
|
||||
## Documentation
|
||||
|
||||
To build the fat EAR bundle it is sufficient to type
|
||||
|
||||
mvn clean package
|
||||
|
||||
## Change log
|
||||
|
||||
See [CHANGELOG.md](CHANGELOG.md).
|
||||
|
||||
## Authors
|
||||
|
||||
* **Mauro Mugnaini** ([Nubisware S.r.l.](http://www.nubisware.com))
|
||||
|
||||
## How to Cite this Software
|
||||
[Intentionally left blank]
|
||||
|
||||
## License
|
||||
|
||||
This project is licensed under the EUPL V.1.1 License - see the [LICENSE.md](LICENSE.md) file for details.
|
||||
|
||||
## About the gCube Framework
|
||||
This software is part of the [gCubeFramework](https://www.gcube-system.org/ "gCubeFramework"): an
|
||||
open-source software toolkit used for building and operating Hybrid Data
|
||||
Infrastructures enabling the dynamic deployment of Virtual Research Environments
|
||||
by favouring the realisation of reuse oriented policies.
|
||||
|
||||
The projects leading to this software have received funding from a series of European Union programmes see [FUNDING.md](FUNDING.md)
|
||||
|
||||
## Acknowledgments
|
||||
[Intentionally left blank]
|
@ -0,0 +1,70 @@
|
||||
<jboss-deployment-structure>
|
||||
<deployment>
|
||||
<module-alias name="deployment.d4science.spi" />
|
||||
</deployment>
|
||||
<sub-deployment name="avatar-importer.jar">
|
||||
<dependencies>
|
||||
<module name="com.google.guava" />
|
||||
<module name="org.apache.commons.codec" />
|
||||
<module name="org.apache.commons.lang" />
|
||||
<module name="org.apache.commons.io" />
|
||||
<module name="org.gcube.keycloak.avatar-storage" />
|
||||
<module name="org.keycloak.keycloak-core" />
|
||||
<module name="org.keycloak.keycloak-server-spi" />
|
||||
<module name="org.keycloak.keycloak-server-spi-private" />
|
||||
<module name="org.keycloak.keycloak-services" />
|
||||
<module name="org.jboss.logging" />
|
||||
<module name="org.slf4j" />
|
||||
</dependencies>
|
||||
</sub-deployment>
|
||||
<sub-deployment name="avatar-realm-resource.jar">
|
||||
<dependencies>
|
||||
<module name="javax.servlet.api" />
|
||||
<module name="javax.ws.rs.api" />
|
||||
<module name="org.apache.commons.io" />
|
||||
<module name="org.gcube.keycloak.avatar-storage" />
|
||||
<module name="org.hibernate" />
|
||||
<module name="org.keycloak.keycloak-core" />
|
||||
<module name="org.keycloak.keycloak-ldap-federation" />
|
||||
<module name="org.keycloak.keycloak-server-spi" />
|
||||
<module name="org.keycloak.keycloak-server-spi-private" />
|
||||
<module name="org.keycloak.keycloak-services" />
|
||||
<module name="org.jboss.logging" />
|
||||
<module name="org.jboss.resteasy.resteasy-jaxb-provider" />
|
||||
<module name="org.jboss.resteasy.resteasy-jaxrs" />
|
||||
<module name="org.jboss.resteasy.resteasy-multipart-provider" />
|
||||
<module name="org.slf4j" />
|
||||
</dependencies>
|
||||
</sub-deployment>
|
||||
<sub-deployment name="event-listener-provider.jar">
|
||||
<dependencies>
|
||||
<module name="org.keycloak.keycloak-core" />
|
||||
<module name="org.keycloak.keycloak-server-spi" />
|
||||
<module name="org.keycloak.keycloak-server-spi-private" />
|
||||
<module name="org.keycloak.keycloak-services" />
|
||||
<module name="org.jboss.logging" />
|
||||
<module name="org.slf4j" />
|
||||
</dependencies>
|
||||
</sub-deployment>
|
||||
<sub-deployment name="identity-provider-mapper.jar">
|
||||
<dependencies>
|
||||
<module name="org.keycloak.keycloak-core" />
|
||||
<module name="org.keycloak.keycloak-server-spi" />
|
||||
<module name="org.keycloak.keycloak-server-spi-private" />
|
||||
<module name="org.keycloak.keycloak-services" />
|
||||
<module name="org.jboss.logging" />
|
||||
<module name="org.slf4j" />
|
||||
</dependencies>
|
||||
</sub-deployment>
|
||||
<sub-deployment name="ldap-storage-mapper.jar">
|
||||
<dependencies>
|
||||
<module name="org.keycloak.keycloak-core" />
|
||||
<module name="org.keycloak.keycloak-ldap-federation" />
|
||||
<module name="org.keycloak.keycloak-server-spi" />
|
||||
<module name="org.keycloak.keycloak-server-spi-private" />
|
||||
<module name="org.keycloak.keycloak-services" />
|
||||
<module name="org.jboss.logging" />
|
||||
<module name="org.slf4j" />
|
||||
</dependencies>
|
||||
</sub-deployment>
|
||||
</jboss-deployment-structure>
|
@ -1,53 +0,0 @@
|
||||
<jboss-deployment-structure>
|
||||
<deployment>
|
||||
<module-alias name="deployment.d4science.spi" />
|
||||
</deployment>
|
||||
<!-- <sub-deployment name="avatar-realm-resource.jar"> -->
|
||||
<!-- <dependencies> -->
|
||||
<!-- <module name="javax.servlet.api" /> -->
|
||||
<!-- <module name="javax.ws.rs.api" /> -->
|
||||
<!-- <module name="org.hibernate" /> -->
|
||||
<!-- <module name="org.keycloak.keycloak-core" /> -->
|
||||
<!-- <module name="org.keycloak.keycloak-ldap-federation" /> -->
|
||||
<!-- <module name="org.keycloak.keycloak-server-spi" /> -->
|
||||
<!-- <module name="org.keycloak.keycloak-server-spi-private" /> -->
|
||||
<!-- <module name="org.keycloak.keycloak-services" /> -->
|
||||
<!-- <module name="org.jboss.logging" /> -->
|
||||
<!-- <module name="org.jboss.resteasy.resteasy-jaxb-provider" /> -->
|
||||
<!-- <module name="org.jboss.resteasy.resteasy-jaxrs" /> -->
|
||||
<!-- <module name="org.jboss.resteasy.resteasy-multipart-provider" /> -->
|
||||
<!-- <module name="org.slf4j" /> -->
|
||||
<!-- </dependencies> -->
|
||||
<!-- </sub-deployment> -->
|
||||
<sub-deployment name="event-listener-provider.jar">
|
||||
<dependencies>
|
||||
<module name="org.keycloak.keycloak-core" />
|
||||
<module name="org.keycloak.keycloak-server-spi" />
|
||||
<module name="org.keycloak.keycloak-server-spi-private" />
|
||||
<module name="org.keycloak.keycloak-services" />
|
||||
<module name="org.jboss.logging" />
|
||||
<module name="org.slf4j" />
|
||||
</dependencies>
|
||||
</sub-deployment>
|
||||
<sub-deployment name="identity-provider-mapper.jar">
|
||||
<dependencies>
|
||||
<module name="org.keycloak.keycloak-core" />
|
||||
<module name="org.keycloak.keycloak-server-spi" />
|
||||
<module name="org.keycloak.keycloak-server-spi-private" />
|
||||
<module name="org.keycloak.keycloak-services" />
|
||||
<module name="org.jboss.logging" />
|
||||
<module name="org.slf4j" />
|
||||
</dependencies>
|
||||
</sub-deployment>
|
||||
<sub-deployment name="ldap-storage-mapper.jar">
|
||||
<dependencies>
|
||||
<module name="org.keycloak.keycloak-core" />
|
||||
<module name="org.keycloak.keycloak-ldap-federation" />
|
||||
<module name="org.keycloak.keycloak-server-spi" />
|
||||
<module name="org.keycloak.keycloak-server-spi-private" />
|
||||
<module name="org.keycloak.keycloak-services" />
|
||||
<module name="org.jboss.logging" />
|
||||
<module name="org.slf4j" />
|
||||
</dependencies>
|
||||
</sub-deployment>
|
||||
</jboss-deployment-structure>
|
@ -0,0 +1,6 @@
|
||||
This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
# Changelog for "keycloak-extension-spi"
|
||||
|
||||
## [v0.2.0-SNAPSHOT]
|
||||
- Extracted sub-module from the original project. Uses the LDAP storage mapper SPI to provide a new `user attribute templated ldap mapper`.
|
@ -0,0 +1,26 @@
|
||||
# Acknowledgments
|
||||
|
||||
The projects leading to this software have received funding from a series of European Union programmes including:
|
||||
|
||||
- the Sixth Framework Programme for Research and Technological Development
|
||||
- [DILIGENT](https://cordis.europa.eu/project/id/004260) (grant no. 004260).
|
||||
- the Seventh Framework Programme for research, technological development and demonstration
|
||||
- [D4Science](https://cordis.europa.eu/project/id/212488) (grant no. 212488);
|
||||
- [D4Science-II](https://cordis.europa.eu/project/id/239019) (grant no.239019);
|
||||
- [ENVRI](https://cordis.europa.eu/project/id/283465) (grant no. 283465);
|
||||
- [iMarine](https://cordis.europa.eu/project/id/283644) (grant no. 283644);
|
||||
- [EUBrazilOpenBio](https://cordis.europa.eu/project/id/288754) (grant no. 288754).
|
||||
- the H2020 research and innovation programme
|
||||
- [SoBigData](https://cordis.europa.eu/project/id/654024) (grant no. 654024);
|
||||
- [PARTHENOS](https://cordis.europa.eu/project/id/654119) (grant no. 654119);
|
||||
- [EGI-Engage](https://cordis.europa.eu/project/id/654142) (grant no. 654142);
|
||||
- [ENVRI PLUS](https://cordis.europa.eu/project/id/654182) (grant no. 654182);
|
||||
- [BlueBRIDGE](https://cordis.europa.eu/project/id/675680) (grant no. 675680);
|
||||
- [PerformFISH](https://cordis.europa.eu/project/id/727610) (grant no. 727610);
|
||||
- [AGINFRA PLUS](https://cordis.europa.eu/project/id/731001) (grant no. 731001);
|
||||
- [DESIRA](https://cordis.europa.eu/project/id/818194) (grant no. 818194);
|
||||
- [ARIADNEplus](https://cordis.europa.eu/project/id/823914) (grant no. 823914);
|
||||
- [RISIS 2](https://cordis.europa.eu/project/id/824091) (grant no. 824091);
|
||||
- [EOSC-Pillar](https://cordis.europa.eu/project/id/857650) (grant no. 857650);
|
||||
- [Blue Cloud](https://cordis.europa.eu/project/id/862409) (grant no. 862409);
|
||||
- [SoBigData-PlusPlus](https://cordis.europa.eu/project/id/871042) (grant no. 871042);
|
@ -0,0 +1,44 @@
|
||||
# Event Publisher Portal
|
||||
|
||||
**LDAP Storage Mapper** extends the [Keycloak](https://www.keycloak.org)'s LDAP storage mapper SPI to provide a new `user attribute templated ldap mapper` that can be used for example to map user's home attribute to the LDAP starting from the username and a proper template.
|
||||
|
||||
## Structure of the project
|
||||
|
||||
The source code is present in `src` folder.
|
||||
|
||||
## Built With
|
||||
|
||||
* [OpenJDK](https://openjdk.java.net/) - The JDK used
|
||||
* [Maven](https://maven.apache.org/) - Dependency Management
|
||||
|
||||
## Documentation
|
||||
|
||||
To build the JAR it is sufficient to type
|
||||
|
||||
mvn clean package
|
||||
|
||||
## Change log
|
||||
|
||||
See [CHANGELOG.md](CHANGELOG.md).
|
||||
|
||||
## Authors
|
||||
|
||||
* **Mauro Mugnaini** ([Nubisware S.r.l.](http://www.nubisware.com))
|
||||
|
||||
## How to Cite this Software
|
||||
[Intentionally left blank]
|
||||
|
||||
## License
|
||||
|
||||
This project is licensed under the EUPL V.1.1 License - see the [LICENSE.md](LICENSE.md) file for details.
|
||||
|
||||
## About the gCube Framework
|
||||
This software is part of the [gCubeFramework](https://www.gcube-system.org/ "gCubeFramework"): an
|
||||
open-source software toolkit used for building and operating Hybrid Data
|
||||
Infrastructures enabling the dynamic deployment of Virtual Research Environments
|
||||
by favouring the realisation of reuse oriented policies.
|
||||
|
||||
The projects leading to this software have received funding from a series of European Union programmes see [FUNDING.md](FUNDING.md)
|
||||
|
||||
## Acknowledgments
|
||||
[Intentionally left blank]
|
@ -1,25 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Resource xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
|
||||
<ID></ID>
|
||||
<Type>Portlet</Type>
|
||||
<Profile>
|
||||
<Description>${project.description}</Description>
|
||||
<Class>PortletUser</Class>
|
||||
<Name>${project.artifactId}</Name>
|
||||
<Version>1.0.0</Version>
|
||||
<Packages>
|
||||
<Software>
|
||||
<Name>${project.artifactId}</Name>
|
||||
<Description>${project.description}</Description>
|
||||
<MavenCoordinates>
|
||||
<groupId>${project.groupId}</groupId>
|
||||
<artifactId>${project.artifactId}</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</MavenCoordinates>
|
||||
<Files>
|
||||
<File>${project.build.finalName}.${project.packaging}</File>
|
||||
</Files>
|
||||
</Software>
|
||||
</Packages>
|
||||
</Profile>
|
||||
</Resource>
|
@ -1,18 +0,0 @@
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-assembly-plugin</artifactId>
|
||||
<configuration>
|
||||
<descriptors>
|
||||
<descriptor>descriptor.xml</descriptor>
|
||||
</descriptors>
|
||||
</configuration>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>servicearchive</id>
|
||||
<phase>install</phase>
|
||||
<goals>
|
||||
<goal>single</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
Loading…
Reference in New Issue