Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

✨ Use HTTP API for userli user lookup #22

Merged
merged 7 commits into from
May 20, 2024
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 0 additions & 16 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,23 +20,7 @@ services:
- ./keycloak/conf/quarkus.properties:/opt/keycloak/conf/quarkus.properties:ro
- ./keycloak/keywind.jar:/opt/keycloak/providers/keywind.jar
- ./keycloak/systemli.jar:/opt/keycloak/providers/systemli.jar
depends_on:
- userli

userli:
image: mariadb:10.3
environment:
MYSQL_USER: mail
MYSQL_PASSWORD: password
MYSQL_DATABASE: mail
MARIADB_RANDOM_ROOT_PASSWORD: true
volumes:
- ./mariadb/dump.sql:/docker-entrypoint-initdb.d/dump.sql
- userli:/var/lib/mysql

networks:
default:
name: keycloak

volumes:
userli:
45 changes: 39 additions & 6 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,36 @@
<version>${version.keycloak}</version>
<scope>provided</scope>
</dependency>
<dependency>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These dependencies are not needed. Also, the Jakarta Sodium dependencies can be safely removed.

<groupId>org.keycloak</groupId>
<artifactId>keycloak-server-spi-private</artifactId>
<version>${version.keycloak}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-services</artifactId>
<version>${version.keycloak}</version>
<scope>provided</scope>
<exclusions>
<exclusion>
<groupId>org.jboss.resteasy</groupId>
<artifactId>*</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-model-jpa</artifactId>
<version>${version.keycloak}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
<version>4.5.14</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.jboss.logging</groupId>
<artifactId>jboss-logging</artifactId>
Expand All @@ -46,18 +70,27 @@
<artifactId>resource-loader</artifactId>
<version>2.0.2</version>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
<version>5.10.2</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.32</version>
<scope>provided</scope>
</dependency>
<dependency>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This can also be removed as we need the logging only for development purposes.

<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>2.0.12</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-reload4j</artifactId>
<version>2.0.12</version>
</dependency>
<dependency>
<groupId>io.rest-assured</groupId>
<artifactId>rest-assured</artifactId>
<version>5.4.0</version>
</dependency>
<dependency>
<groupId>jakarta.persistence</groupId>
<artifactId>jakarta.persistence-api</artifactId>
Expand Down
10 changes: 10 additions & 0 deletions src/main/java/org/systemli/keycloak/Constants.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package org.systemli.keycloak;

import lombok.experimental.UtilityClass;

@UtilityClass
public class Constants {
public String REALM_DOMAIN = "userliRealmDomain";
public String BASE_URL = "userliBaseUrl";
public String KEYCLOAK_API_TOKEN = "userliKeycloakToken";
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
public String REALM_DOMAIN = "userliRealmDomain";
public String BASE_URL = "userliBaseUrl";
public String KEYCLOAK_API_TOKEN = "userliKeycloakToken";
public final String REALM_DOMAIN = "userliRealmDomain";
public final String BASE_URL = "userliBaseUrl";
public final String KEYCLOAK_API_TOKEN = "userliKeycloakToken";

Constants should be final to ensure they cannot be modified.

}
27 changes: 0 additions & 27 deletions src/main/java/org/systemli/keycloak/PasswordVerifier.java

This file was deleted.

17 changes: 0 additions & 17 deletions src/main/java/org/systemli/keycloak/UserliDomain.java

This file was deleted.

28 changes: 2 additions & 26 deletions src/main/java/org/systemli/keycloak/UserliUser.java
Original file line number Diff line number Diff line change
@@ -1,37 +1,13 @@
package org.systemli.keycloak;

import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.NamedQuery;
import jakarta.persistence.Table;
import java.util.List;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.NonNull;

@Data
@NoArgsConstructor
@NamedQuery(name = "findUserByEmailAndDomain", query = "select u from UserliUser u where u.email = :email and u.domain = :domain")
@NamedQuery(name = "findUsersByEmailAndDomain", query = "select u from UserliUser u where u.email like :email and u.domain = :domain")
@NamedQuery(name = "findUserByUsernameAndDomain", query = "select u from UserliUser u where u.email like :username and u.domain = :domain")
@NamedQuery(name = "findUserByIdAndDomain", query = "select u from UserliUser u where u.id = :id and u.domain = :domain")
@NamedQuery(name = "countUsersByDomain", query = "select count(u) from UserliUser u where u.domain = :domain")
@Table(name = "virtual_users")
@Entity
public class UserliUser {
@Id
@NonNull
private String id;
@NonNull
private String email;
@NonNull
private String password;
@NonNull
private Boolean deleted;
@ManyToOne
@JoinColumn(name = "domain_id")
private UserliDomain domain;
private List<String> roles;

public String getUsername() {
return email.substring(0, email.indexOf('@'));
Expand Down
5 changes: 0 additions & 5 deletions src/main/java/org/systemli/keycloak/UserliUserAdapter.java
Original file line number Diff line number Diff line change
Expand Up @@ -39,11 +39,6 @@ public boolean isEmailVerified() {
return true;
}

@Override
public boolean isEnabled() {
return !user.getDeleted();
}

@Override
public Map<String, List<String>> getAttributes() {
MultivaluedHashMap<String, String> attributes = new MultivaluedHashMap<>();
Expand Down
34 changes: 34 additions & 0 deletions src/main/java/org/systemli/keycloak/UserliUserClient.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package org.systemli.keycloak;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This interface only makes sense when we would expose an API in Keycloak, which we don't do. So this makes no sense here.


import jakarta.ws.rs.Consumes;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.PathParam;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.QueryParam;
import jakarta.ws.rs.core.MediaType;

import java.util.List;

@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
public interface UserliUserClient {

@GET
@Path("/api/keycloak/{domain}")
List<UserliUser> getUsers(@QueryParam("search") String search, @QueryParam("first") int first, @QueryParam("max") int max);

@GET
@Path("/api/keycloak/{domain}/count")
Integer getUsersCount();

@GET
@Path("/api/keycloak/{domain}/user/{id}")
UserliUser getUserById(@PathParam("id") String id);

@POST
@Path("/api/keycloak/{domain}/validate/${email}")
Boolean validate(@PathParam("email") String email, @QueryParam("password") String password);

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
package org.systemli.keycloak;

import com.fasterxml.jackson.core.type.TypeReference;
import jakarta.ws.rs.WebApplicationException;
import jakarta.ws.rs.core.Response;
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 UserliUserClientSimpleHttp implements UserliUserClient {

private final CloseableHttpClient httpClient;
private final String realmDomain;
private final String baseUrl;
private final String keycloakApiToken;

public UserliUserClientSimpleHttp(KeycloakSession session, ComponentModel model) {
this.httpClient = session.getProvider(HttpClientProvider.class).getHttpClient();
this.realmDomain = model.get(Constants.REALM_DOMAIN);
this.baseUrl = model.get(Constants.BASE_URL);
this.keycloakApiToken = model.get(Constants.KEYCLOAK_API_TOKEN);
}

@Override
@SneakyThrows
public List<UserliUser> getUsers(String search, int first, int max) {
String url = String.format("%s/api/keycloak/%s", baseUrl, realmDomain);
SimpleHttp simpleHttp = SimpleHttp.doGet(url, httpClient)
.auth(keycloakApiToken)
.param("first", String.valueOf(first))
.param("max", String.valueOf(max));
if (search != null) {
simpleHttp.param("search", search);
}
return simpleHttp.asJson(new TypeReference<>() {});
}

@Override
@SneakyThrows
public Integer getUsersCount() {
String url = String.format("%s/api/keycloak/%s/count", baseUrl, realmDomain);
String count = SimpleHttp.doGet(url, httpClient)
.auth(keycloakApiToken)
.asString();
return Integer.valueOf(count);
}

@Override
@SneakyThrows
public UserliUser getUserById(String id) {
String url = String.format("%s/api/keycloak/%s/user/%s", baseUrl, realmDomain, id);
SimpleHttp.Response response = SimpleHttp.doGet(url, httpClient)
.auth(keycloakApiToken)
.asResponse();
if (response.getStatus() == 404) {
throw new WebApplicationException(response.getStatus());
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This exception is the only reason why we still need the Jakarta package. So I would recommend creating an exception on our own.

}
return response.asJson(UserliUser.class);
}

@Override
@SneakyThrows
public Boolean validate(String email, String password) {
log.warn("UserliUserClientSimpleHttp validate: User with email '{}' and password '{}' tries to login", email, password);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
log.warn("UserliUserClientSimpleHttp validate: User with email '{}' and password '{}' tries to login", email, password);

Security! :-)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Haha ouch, this was a debugging leftover 🙈

String url = String.format("%s/api/keycloak/%s/validate/%s", baseUrl, realmDomain, email);
SimpleHttp.Response response = SimpleHttp.doPost(url, httpClient)
.auth(keycloakApiToken)
.param("password", password)
.asResponse();
if (response.getStatus() == 200) {
return true;
}
return false;
}

}
Loading