Skip to content

Commit

Permalink
http-server facade for user repo
Browse files Browse the repository at this point in the history
  • Loading branch information
dasniko committed Oct 6, 2023
1 parent e60e82b commit 4c0d29a
Show file tree
Hide file tree
Showing 14 changed files with 658 additions and 56 deletions.
4 changes: 4 additions & 0 deletions flintstones-userprovider/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,10 @@
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>com.google.auto.service</groupId>
<artifactId>auto-service</artifactId>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ public class FlintstoneUserAdapter extends AbstractUserAdapterFederatedStorage {

public FlintstoneUserAdapter(KeycloakSession session, RealmModel realm, ComponentModel model, FlintstoneUser user) {
super(session, realm, model);
this.storageId = new StorageId(storageProviderModel.getId(), user.getUsername());
this.storageId = new StorageId(storageProviderModel.getId(), user.getId());
this.user = user;
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
package dasniko.keycloak.user.flintstones;

import dasniko.keycloak.user.flintstones.repo.Credential;
import dasniko.keycloak.user.flintstones.repo.FlintstoneUser;
import dasniko.keycloak.user.flintstones.repo.FlintstonesRepository;
import dasniko.keycloak.user.flintstones.repo.FlintstonesApiClient;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.keycloak.component.ComponentModel;
Expand Down Expand Up @@ -42,7 +43,7 @@ public class FlintstonesUserStorageProvider implements UserStorageProvider,

private final KeycloakSession session;
private final ComponentModel model;
private final FlintstonesRepository repository;
private final FlintstonesApiClient apiClient;

// map of loaded users in this transaction
private final Map<String, UserModel> loadedUsers = new HashMap<>();
Expand All @@ -64,7 +65,8 @@ public boolean isValid(RealmModel realm, UserModel user, CredentialInput input)
if (!supportsCredentialType(input.getType()) || !(input instanceof UserCredentialModel cred)) {
return false;
}
return repository.validateCredentials(user.getUsername(), cred.getChallengeResponse());
Credential credential = new Credential("password", cred.getChallengeResponse());
return apiClient.verifyCredentials(StorageId.externalId(user.getId()), credential);
}

@Override
Expand All @@ -88,7 +90,8 @@ public boolean updateCredential(RealmModel realm, UserModel user, CredentialInpu
}
}

return repository.updateCredentials(user.getUsername(), cred.getChallengeResponse());
Credential credential = new Credential("password", cred.getChallengeResponse());
return apiClient.updateCredentials(StorageId.externalId(user.getId()), credential);
}

@Override
Expand All @@ -103,17 +106,17 @@ public Stream<String> getDisableableCredentialTypesStream(RealmModel realm, User
@Override
public UserModel getUserById(RealmModel realm, String id) {
String externalId = StorageId.externalId(id);
return findUser(realm, externalId, repository::findUserByUsernameOrEmail);
return findUser(realm, externalId, apiClient::getUserById);
}

@Override
public UserModel getUserByUsername(RealmModel realm, String username) {
return findUser(realm, username, repository::findUserByUsernameOrEmail);
return findUser(realm, username, apiClient::getUserByUsername);
}

@Override
public UserModel getUserByEmail(RealmModel realm, String email) {
return getUserByUsername(realm, email);
return findUser(realm, email, apiClient::getUserByEmail);
}

private UserModel findUser(RealmModel realm, String identifier, Function<String, FlintstoneUser> fnFindUser) {
Expand All @@ -133,18 +136,16 @@ private UserModel findUser(RealmModel realm, String identifier, Function<String,

@Override
public int getUsersCount(RealmModel realm) {
return repository.getUsersCount();
return apiClient.usersCount();
}

@Override
public Stream<UserModel> searchForUserStream(RealmModel realm, Map<String, String> params, Integer firstResult, Integer maxResults) {
List<FlintstoneUser> result;
if (params.containsKey(UserModel.USERNAME)) {
result = List.of(repository.findUserByUsernameOrEmail(params.get(UserModel.USERNAME)));
} else if (params.containsKey(UserModel.SEARCH)) {
result = repository.findUsers(params.get(UserModel.SEARCH));
result = List.of(apiClient.getUserByUsername(params.get(UserModel.USERNAME)));
} else {
result = repository.getAllUsers();
result = apiClient.searchUsers(params.getOrDefault(UserModel.SEARCH, null), firstResult, maxResults);
}
return result.stream().map(user -> new FlintstoneUserAdapter(session, realm, model, user));
}
Expand All @@ -164,8 +165,9 @@ public UserModel addUser(RealmModel realm, String username) {
if (syncUsers()) {
FlintstoneUser flintstoneUser = new FlintstoneUser();
flintstoneUser.setUsername(username);
apiClient.createUser(flintstoneUser);
flintstoneUser = apiClient.getUserByUsername(username);
FlintstoneUserAdapter newUser = new FlintstoneUserAdapter(session, realm, model, flintstoneUser);
repository.createUser(flintstoneUser);
newUsers.add(newUser);
loadedUsers.put(username, newUser);
return newUser;
Expand All @@ -176,19 +178,19 @@ public UserModel addUser(RealmModel realm, String username) {
@Override
public boolean removeUser(RealmModel realm, UserModel user) {
String externalId = StorageId.externalId(user.getId());
return repository.removeUser(externalId);
return apiClient.deleteUser(externalId);
}

@Override
public void close() {
newUsers.forEach(newUser -> {
repository.updateUser(newUser.getUser());
apiClient.updateUser(newUser.getUser());
loadedUsers.remove(newUser.getUsername());
});
loadedUsers.values().forEach(user -> {
FlintstoneUserAdapter userAdapter = (FlintstoneUserAdapter) user;
if (userAdapter.isDirty()) {
repository.updateUser(userAdapter.getUser());
apiClient.updateUser(userAdapter.getUser());
}
});
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package dasniko.keycloak.user.flintstones;

import dasniko.keycloak.user.flintstones.repo.FlintstonesRepository;
import com.google.auto.service.AutoService;
import dasniko.keycloak.user.flintstones.repo.FlintstonesApiClient;
import dasniko.keycloak.user.flintstones.repo.FlintstonesApiServer;
import org.keycloak.component.ComponentModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
Expand All @@ -13,18 +15,21 @@
/**
* @author Niko Köbler, http://www.n-k.de, @dasniko
*/
@AutoService(UserStorageProviderFactory.class)
public class FlintstonesUserStorageProviderFactory implements UserStorageProviderFactory<FlintstonesUserStorageProvider> {

public static final String PROVIDER_ID = "the-flintstones";

public static final String USER_API_BASE_URL = "apiBaseUrl";
static final String USER_CREATION_ENABLED = "userCreation";
static final String USE_PASSWORD_POLICY = "usePasswordPolicy";

private FlintstonesRepository repository;
private FlintstonesApiServer apiServer;

@Override
public FlintstonesUserStorageProvider create(KeycloakSession session, ComponentModel model) {
return new FlintstonesUserStorageProvider(session, model, repository);
FlintstonesApiClient apiClient = new FlintstonesApiClient(session, model);
return new FlintstonesUserStorageProvider(session, model, apiClient);
}

@Override
Expand All @@ -34,14 +39,22 @@ public String getId() {

@Override
public void postInit(KeycloakSessionFactory factory) {
repository = new FlintstonesRepository();
apiServer = new FlintstonesApiServer();
}

@Override
public List<ProviderConfigProperty> getConfigProperties() {
return ProviderConfigurationBuilder.create()
.property(USER_API_BASE_URL, "API Base URL", "", ProviderConfigProperty.STRING_TYPE, "http://localhost:8000", null)
.property(USER_CREATION_ENABLED, "Sync Registrations", "Should newly created users be created within this store?", ProviderConfigProperty.BOOLEAN_TYPE, "false", null)
.property(USE_PASSWORD_POLICY, "Validate password policy", "Determines if Keycloak should validate the password with the realm password policy before updating it.", ProviderConfigProperty.BOOLEAN_TYPE, "false", null)
.build();
}

@Override
public void close() {
if (apiServer != null) {
apiServer.stop();
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package dasniko.keycloak.user.flintstones.repo;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@NoArgsConstructor
@AllArgsConstructor
public class Credential {
private String type;
private String value;
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
@NoArgsConstructor
public class FlintstoneUser implements Cloneable {

private String id;
private String username;
private String email;
private String firstName;
Expand All @@ -22,7 +23,8 @@ public class FlintstoneUser implements Cloneable {
private Long created;
private List<String> roles;

public FlintstoneUser(String email, String firstName, String lastName, boolean enabled, List<String> roles) {
public FlintstoneUser(String id, String email, String firstName, String lastName, boolean enabled, List<String> roles) {
this.id = id;
this.username = email.substring(0, email.indexOf("@"));
this.email = email;
this.firstName = firstName;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
package dasniko.keycloak.user.flintstones.repo;

import com.fasterxml.jackson.core.type.TypeReference;
import dasniko.keycloak.user.flintstones.FlintstonesUserStorageProviderFactory;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.apache.http.impl.client.CloseableHttpClient;
import org.keycloak.broker.provider.util.SimpleHttp;
import org.keycloak.component.ComponentModel;
import org.keycloak.connections.httpclient.HttpClientProvider;
import org.keycloak.models.KeycloakSession;

import java.util.List;

@Slf4j
public class FlintstonesApiClient {

private final CloseableHttpClient httpClient;
private final String baseUrl;

public FlintstonesApiClient(KeycloakSession session, ComponentModel model) {
this.httpClient = session.getProvider(HttpClientProvider.class).getHttpClient();
this.baseUrl = model.get(FlintstonesUserStorageProviderFactory.USER_API_BASE_URL);
}

@SneakyThrows
public List<FlintstoneUser> searchUsers(String search, int first, int max) {
String url = String.format("%s/users", baseUrl);
SimpleHttp simpleHttp = prepareGetRequest(url);
if (first >= 0) {
simpleHttp.param("first", String.valueOf(first));
}
if (max >= 0) {
simpleHttp.param("max", String.valueOf(max));
}
if (search != null) {
simpleHttp.param("search", search);
}
return simpleHttp.asJson(new TypeReference<>() {});
}

@SneakyThrows
public Integer usersCount() {
String url = String.format("%s/users/count", baseUrl);
String count = prepareGetRequest(url).asString();
return Integer.valueOf(count);
}

@SneakyThrows
public boolean createUser(FlintstoneUser user) {
String url = String.format("%s/users", baseUrl);
return SimpleHttp.doPost(url, httpClient).json(user).asStatus() == 201;
}

@SneakyThrows
public FlintstoneUser getUserById(String userId) {
String url = String.format("%s/users/%s", baseUrl, userId);
try (SimpleHttp.Response response = prepareGetRequest(url).asResponse()) {
if (response.getStatus() >= 400) {
return null;
}
return response.asJson(FlintstoneUser.class);
}
}

public FlintstoneUser getUserByUsername(String username) {
return getUserByUsernameOrEmail("username", username);
}

public FlintstoneUser getUserByEmail(String email) {
return getUserByUsernameOrEmail("email", email);
}

@SneakyThrows
private FlintstoneUser getUserByUsernameOrEmail(String field, String value) {
String url = String.format("%s/users", baseUrl);
SimpleHttp simpleHttp = prepareGetRequest(url);
simpleHttp.param(field, value);
List<FlintstoneUser> result = simpleHttp.asJson(new TypeReference<>() {});
return result.isEmpty() ? null : result.get(0);
}

@SneakyThrows
public boolean updateUser(FlintstoneUser user) {
String url = String.format("%s/users/%s", baseUrl, user.getId());
return SimpleHttp.doPut(url, httpClient).json(user).asStatus() == 204;
}

@SneakyThrows
public boolean deleteUser(String userId) {
String url = String.format("%s/users/%s", baseUrl, userId);
return SimpleHttp.doDelete(url, httpClient).asStatus() == 204;
}

@SneakyThrows
public boolean verifyCredentials(String userId, Credential credential) {
String url = String.format("%s/users/%s/credentials/verify", baseUrl, userId);
return SimpleHttp.doPost(url, httpClient).json(credential).asStatus() == 204;
}

@SneakyThrows
public boolean updateCredentials(String userId, Credential credential) {
String url = String.format("%s/users/%s/credentials", baseUrl, userId);
return SimpleHttp.doPut(url, httpClient).json(credential).asStatus() == 204;
}

private SimpleHttp prepareGetRequest(String url) {
return SimpleHttp.doGet(url, httpClient).acceptJson();
}
}
Loading

0 comments on commit 4c0d29a

Please sign in to comment.