Beta version of storage provider that saves/loads avatars from a S3 bucket

master
Mauro Mugnaini 7 months ago
parent 157e5f46aa
commit 382cbe5f8d

@ -0,0 +1,126 @@
package org.gcube.keycloak.avatar.storage.s3;
import java.io.InputStream;
import org.gcube.keycloak.avatar.storage.AvatarStorageProvider;
import org.jboss.logging.Logger;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
import io.minio.GetObjectArgs;
import io.minio.MinioClient;
import io.minio.PutObjectArgs;
import io.minio.RemoveObjectArgs;
import io.minio.errors.ErrorResponseException;
public class MinioAvatarStorageProvider implements AvatarStorageProvider {
private static final Logger logger = Logger.getLogger(MinioAvatarStorageProvider.class);
private static final String AVATAR_FOLDER = "avatar";
private static final int CHUNK_SIZE = 5242880;
private final Configuration configuration;
public MinioAvatarStorageProvider(Configuration configuration) {
this.configuration = configuration;
}
@Override
public void saveAvatarImage(RealmModel realmModel, UserModel userModel, InputStream input) {
execute(minioClient -> minioClient
.putObject(PutObjectArgs.builder().bucket(configuration.rootBucket).object(getAvatarFilePath(
realmModel, userModel)).stream(input, -1, CHUNK_SIZE).build()));
}
@Override
public InputStream loadAvatarImage(RealmModel realmModel, UserModel userModel) {
logger.debug("Loading avatar from S3");
return execute(new Executor<InputStream>() {
public InputStream execute(MinioClient minioClient) throws Exception {
try {
return minioClient
.getObject(GetObjectArgs.builder().bucket(configuration.rootBucket).object(getAvatarFilePath(
realmModel, userModel)).build());
} catch (ErrorResponseException e) {
if (e.response().code() == 404) {
logger.debugf("Avatar file not found for user '%s' in realm '%s'", userModel.getUsername(), realmModel.getName());
return null;
} else {
throw e;
}
}
};
});
}
@Override
public void deleteAvatarImage(RealmModel realmModel, UserModel userModel) {
logger.debug("Deeleting avatar from S3");
execute(minioClient -> {
minioClient.removeObject(RemoveObjectArgs.builder().bucket(configuration.rootBucket)
.object(getAvatarFilePath(realmModel, userModel)).build());
return true;
});
}
@Override
public void close() {
// NOOP
}
public String getAvatarFilePath(RealmModel realmModel, UserModel userModel) {
return AVATAR_FOLDER + "/" + realmModel.getName() + "/" + userModel.getUsername();
}
public <T> T execute(Executor<T> executor) {
try {
MinioClient minioClient = MinioClient.builder()
.endpoint(configuration.getServerUrl())
.credentials(configuration.getAccessKey(), configuration.getSecretKey())
.build();
return executor.execute(minioClient);
} catch (Exception e) {
throw new RuntimeException("Executing operation on S3 persistnce", e);
}
}
public static class Configuration {
private final String serverUrl;
private final String accessKey;
private final String secretKey;
private final String rootBucket;
public Configuration(String serverUrl, String accessKey, String secretKey, String rootBucket) {
this.serverUrl = serverUrl;
this.accessKey = accessKey;
this.secretKey = secretKey;
this.rootBucket = rootBucket;
}
public String getServerUrl() {
return serverUrl;
}
public String getAccessKey() {
return accessKey;
}
public String getSecretKey() {
return secretKey;
}
public String getRootBucket() {
return rootBucket;
}
}
public interface Executor<T> {
T execute(MinioClient minioClient) throws Exception;
}
}

@ -0,0 +1,45 @@
package org.gcube.keycloak.avatar.storage.s3;
import org.gcube.keycloak.avatar.storage.AvatarStorageProvider;
import org.gcube.keycloak.avatar.storage.AvatarStorageProviderFactory;
import org.gcube.keycloak.avatar.storage.s3.MinioAvatarStorageProvider.Configuration;
import org.keycloak.Config;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
public class MinioAvatarStorageProviderFactory implements AvatarStorageProviderFactory {
private Configuration minioConfig;
@Override
public AvatarStorageProvider create(KeycloakSession session) {
return new MinioAvatarStorageProvider(minioConfig);
}
@Override
public void init(Config.Scope config) {
String serverUrl = config.get("server-url");
String accessKey = config.get("access-key");
String secretKey = config.get("secret-key");
String rootBucket = config.get("root-bucket");
this.minioConfig = new Configuration(serverUrl, accessKey, secretKey, rootBucket);
}
@Override
public void postInit(KeycloakSessionFactory factory) {
// NOOP
}
@Override
public void close() {
// NOOP
}
@Override
public String getId() {
return "avatar-storage-s3";
}
}

@ -0,0 +1,49 @@
package org.gcube.keycloak.avatar.storage.s3;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.mockito.Mockito.when;
import java.io.FileNotFoundException;
import org.gcube.keycloak.avatar.storage.s3.MinioAvatarStorageProvider.Configuration;
import org.jboss.logging.Logger;
import org.junit.Test;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
import org.mockito.Mockito;
public class MinioAvatarStorageTest {
private static final Logger logger = Logger.getLogger(MinioAvatarStorageTest.class);
private static final String ENDPOINT_URL = "https://isti-cloud.isti.cnr.it:13808";
private static final String ACCESS_KEY_ENV = "ACCESS_KEY";
private static final String SECRET_KEY_ENV = "SECRET_KEY";
private static final String BUCKET = "d4s-container-registry-dev";
@Test
public void test() throws FileNotFoundException {
String accessKey = System.getenv(ACCESS_KEY_ENV);
String secretKey = System.getenv(SECRET_KEY_ENV);
if (accessKey == null || secretKey == null) {
logger.error("Cannot proceed with tests without access and secret keys");
}
Configuration minioConfig = new Configuration(ENDPOINT_URL, accessKey, secretKey, BUCKET);
MinioAvatarStorageProvider minioAvatarStorageProvider = new MinioAvatarStorageProvider(minioConfig);
RealmModel realmModel = Mockito.mock(RealmModel.class);
when(realmModel.getName()).thenReturn("testRealm");
UserModel userModel = Mockito.mock(UserModel.class);
when(userModel.getUsername()).thenReturn("test.user");
minioAvatarStorageProvider.saveAvatarImage(realmModel, userModel,
this.getClass().getClassLoader().getResourceAsStream("TM_Emoji.jpg"));
assertNotNull(minioAvatarStorageProvider.loadAvatarImage(realmModel, userModel));
minioAvatarStorageProvider.deleteAvatarImage(realmModel, userModel);
assertNull(minioAvatarStorageProvider.loadAvatarImage(realmModel, userModel));
// Delete of a non existing resource must not raise an exception, assuring it with a proper test
minioAvatarStorageProvider.deleteAvatarImage(realmModel, userModel);
}
}

@ -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>

Binary file not shown.

After

Width:  |  Height:  |  Size: 124 KiB

Loading…
Cancel
Save