Skip to content

Commit

Permalink
Added option for KeycloakAuthProvider to locally register user
Browse files Browse the repository at this point in the history
  • Loading branch information
hylkevds committed Nov 24, 2023
1 parent 69e7b66 commit bb4b451
Show file tree
Hide file tree
Showing 10 changed files with 364 additions and 20 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,20 @@
</createTable>
</changeSet>

<changeSet author="scf" id="20231117-users" objectQuotingStrategy="QUOTE_ALL_OBJECTS">
<!-- If there is a users table, but it has no password column. -->
<preConditions onFail="MARK_RAN">
<tableExists tableName="USERS" />
<not>
<columnExists tableName="USERS" columnName="USER_PASS" />
</not>
</preConditions>
<addColumn tableName="USERS">
<column name="USER_PASS" type="VARCHAR(255)" />
</addColumn>
</changeSet>


<changeSet author="scf" id="20181121-user_roles" objectQuotingStrategy="QUOTE_ALL_OBJECTS">
<preConditions onFail="MARK_RAN">
<not>
Expand Down
5 changes: 5 additions & 0 deletions FROST-Server.Auth.Keycloak/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,11 @@
<artifactId>FROST-Server.Core</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>${project.groupId}</groupId>
<artifactId>FROST-Server.SQLjooq</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>${project.groupId}</groupId>
<artifactId>FROST-Server.Util</artifactId>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
/*
* Copyright (C) 2023 Fraunhofer Institut IOSB, Fraunhoferstr. 1, D 76131
* Karlsruhe, Germany.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package de.fraunhofer.iosb.ilt.frostserver.auth.keycloak;

import static de.fraunhofer.iosb.ilt.frostserver.auth.keycloak.KeycloakAuthProvider.TAG_USERNAME_COLUMN;
import static de.fraunhofer.iosb.ilt.frostserver.auth.keycloak.KeycloakAuthProvider.TAG_USER_TABLE;
import static de.fraunhofer.iosb.ilt.frostserver.persistence.pgjooq.utils.ConnectionUtils.TAG_DB_URL;

import de.fraunhofer.iosb.ilt.frostserver.persistence.pgjooq.utils.ConnectionUtils;
import de.fraunhofer.iosb.ilt.frostserver.persistence.pgjooq.utils.ConnectionUtils.ConnectionWrapper;
import de.fraunhofer.iosb.ilt.frostserver.settings.CoreSettings;
import de.fraunhofer.iosb.ilt.frostserver.settings.Settings;
import java.sql.SQLException;
import java.util.HashMap;
import java.util.Map;
import org.jooq.DSLContext;
import org.jooq.Field;
import org.jooq.Record;
import org.jooq.SQLDialect;
import org.jooq.Table;
import org.jooq.impl.DSL;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
*
* @author scf
*/
public class DatabaseHandler {

/**
* The logger for this class.
*/
private static final Logger LOGGER = LoggerFactory.getLogger(DatabaseHandler.class);

private static final Map<CoreSettings, DatabaseHandler> INSTANCES = new HashMap<>();

private final Settings authSettings;
private final String connectionUrl;
private final String userTable;
private final String usernameColumn;

public static void init(CoreSettings coreSettings) {
if (INSTANCES.get(coreSettings) == null) {
createInstance(coreSettings);
}
}

private static synchronized DatabaseHandler createInstance(CoreSettings coreSettings) {
return INSTANCES.computeIfAbsent(coreSettings, (s) -> {
LOGGER.info("Initialising DatabaseHandler.");
return new DatabaseHandler(coreSettings);
});
}

public static DatabaseHandler getInstance(CoreSettings coreSettings) {
DatabaseHandler instance = INSTANCES.get(coreSettings);
if (instance == null) {
LOGGER.error("DatabaseHandler not initialised.");
}
return instance;
}

private DatabaseHandler(CoreSettings coreSettings) {
authSettings = coreSettings.getAuthSettings();
connectionUrl = authSettings.get(TAG_DB_URL, ConnectionUtils.class, false);
userTable = authSettings.get(TAG_USER_TABLE, KeycloakAuthProvider.class);
usernameColumn = authSettings.get(TAG_USERNAME_COLUMN, KeycloakAuthProvider.class);
}

/**
* Checks if the user is registered locally and if not, add the user.
*
* @param username the username
*/
public void enureUserInUsertable(String username) {
try (final ConnectionWrapper connectionProvider = new ConnectionWrapper(authSettings, connectionUrl)) {
final DSLContext dslContext = DSL.using(connectionProvider.get(), SQLDialect.POSTGRES);
final Field<String> usernameField = DSL.field(DSL.name(usernameColumn), String.class);
final Table<Record> table = DSL.table(DSL.name(userTable));
long count = dslContext
.selectCount()
.from(table)
.where(usernameField.eq(username))
.fetchOne()
.component1();
if (count == 0) {
dslContext.insertInto(table)
.set(usernameField, username)
.execute();
connectionProvider.commit();
}
} catch (SQLException | RuntimeException exc) {
LOGGER.error("Failed to register user locally.", exc);
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
import de.fraunhofer.iosb.ilt.frostserver.settings.CoreSettings;
import de.fraunhofer.iosb.ilt.frostserver.settings.Settings;
import de.fraunhofer.iosb.ilt.frostserver.settings.annotation.DefaultValue;
import de.fraunhofer.iosb.ilt.frostserver.settings.annotation.DefaultValueBoolean;
import de.fraunhofer.iosb.ilt.frostserver.settings.annotation.DefaultValueInt;
import de.fraunhofer.iosb.ilt.frostserver.util.AuthProvider;
import de.fraunhofer.iosb.ilt.frostserver.util.LiquibaseUser;
Expand Down Expand Up @@ -79,6 +80,15 @@ public class KeycloakAuthProvider implements AuthProvider, LiquibaseUser, Config
@DefaultValueInt(10)
public static final String TAG_MAX_CLIENTS_PER_USER = "maxClientsPerUser";

@DefaultValueBoolean(false)
public static final String TAG_REGISTER_USER_LOCALLY = "registerUserLocally";

@DefaultValue("USERS")
public static final String TAG_USER_TABLE = "userTable";

@DefaultValue("USER_NAME")
public static final String TAG_USERNAME_COLUMN = "usernameColumn";

/**
* The logger for this class.
*/
Expand All @@ -95,6 +105,8 @@ public class KeycloakAuthProvider implements AuthProvider, LiquibaseUser, Config
private CoreSettings coreSettings;
private String roleAdmin;
private int maxClientsPerUser;
private boolean registerUserLocally;
private DatabaseHandler databaseHandler;

private final Map<String, UserClientInfo> clientidToUserinfo = new ConcurrentHashMap<>();
private final Map<String, UserClientInfo> usernameToUserinfo = new ConcurrentHashMap<>();
Expand All @@ -113,6 +125,11 @@ public void init(CoreSettings coreSettings) {
final Settings authSettings = coreSettings.getAuthSettings();
roleAdmin = authSettings.get(TAG_AUTH_ROLE_ADMIN, CoreSettings.class);
maxClientsPerUser = authSettings.getInt(TAG_MAX_CLIENTS_PER_USER, getClass());
registerUserLocally = authSettings.getBoolean(TAG_REGISTER_USER_LOCALLY, KeycloakAuthProvider.class);
if (registerUserLocally) {
DatabaseHandler.init(coreSettings);
databaseHandler = DatabaseHandler.getInstance(coreSettings);
}
}

@Override
Expand Down Expand Up @@ -173,6 +190,9 @@ private boolean checkLogin(AbstractKeycloakLoginModule loginModule, UserData use
client.setSubject(subject);
CLIENTMAP.put(clientId, client);
client.getSubject().getPrincipals().stream().forEach(t -> userData.roles.add(t.getName()));
if (registerUserLocally) {
databaseHandler.enureUserInUsertable(userData.userName);
}
}
return login;
} catch (LoginException ex) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
*/
package de.fraunhofer.iosb.ilt.frostserver.auth.keycloak;

import static de.fraunhofer.iosb.ilt.frostserver.auth.keycloak.KeycloakAuthProvider.TAG_REGISTER_USER_LOCALLY;
import static de.fraunhofer.iosb.ilt.frostserver.settings.CoreSettings.TAG_AUTHENTICATE_ONLY;
import static de.fraunhofer.iosb.ilt.frostserver.settings.CoreSettings.TAG_AUTH_ALLOW_ANON_READ;
import static de.fraunhofer.iosb.ilt.frostserver.settings.CoreSettings.TAG_AUTH_ROLE_ADMIN;
Expand All @@ -43,7 +44,6 @@
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import org.keycloak.adapters.AdapterDeploymentContext;
Expand Down Expand Up @@ -79,6 +79,8 @@ public class KeycloakFilter implements Filter {
private Map<Role, String> roleMappings;
private String roleAdmin;
private boolean authenticateOnly;
private boolean registerUserLocally;
private DatabaseHandler databaseHandler;

private AdapterDeploymentContext deploymentContext;
private NodesRegistrationManagement nodesRegistrationManagement;
Expand All @@ -96,7 +98,11 @@ public void init(FilterConfig filterConfig) throws ServletException {
Settings authSettings = coreSettings.getAuthSettings();
roleMappings = AuthUtils.loadRoleMapping(authSettings);
roleAdmin = authSettings.get(TAG_AUTH_ROLE_ADMIN, CoreSettings.class);
authenticateOnly = "T".equals(authSettings.get(TAG_AUTHENTICATE_ONLY, "F"));
authenticateOnly = authSettings.getBoolean(TAG_AUTHENTICATE_ONLY, CoreSettings.class);
registerUserLocally = authSettings.getBoolean(TAG_REGISTER_USER_LOCALLY, KeycloakAuthProvider.class);
if (registerUserLocally) {
databaseHandler = DatabaseHandler.getInstance(coreSettings);
}

final boolean anonRead = authSettings.getBoolean(TAG_AUTH_ALLOW_ANON_READ, CoreSettings.class);
roleMappersByPath.put("/Data", method -> Role.ADMIN);
Expand Down Expand Up @@ -209,19 +215,21 @@ public void doFilter(ServletRequest request, ServletResponse response, FilterCha
LOGGER.debug("Request handled by authentication actions.");
return;
} else {
final KeycloakAccount account = findKeycloakAccount(httpRequest);
final Principal principalBasic = account.getPrincipal();
final Set<String> roles = account.getRoles();
final String userName = principalBasic.getName();
final PrincipalExtended pe = new PrincipalExtended(userName, roles.contains(roleAdmin), roles);
if (registerUserLocally) {
databaseHandler.enureUserInUsertable(userName);
}
if (authenticateOnly) {
final KeycloakAccount account = findKeycloakAccount(httpRequest);
final Principal principalBasic = account.getPrincipal();
final Set<String> roles = account.getRoles();
final PrincipalExtended pe = new PrincipalExtended(principalBasic.getName(), roles.contains(roleAdmin), roles);
chain.doFilter(new RequestWrapper(httpRequest, pe), response);
return;
}

HttpServletRequestWrapper wrapper = tokenStore.buildWrapper();
if (wrapper.isUserInRole(roleMappings.get(requiredRole))) {
if (roles.contains(roleMappings.get(requiredRole))) {
LOGGER.debug("User has correct role.");
chain.doFilter(wrapper, response);
chain.doFilter(new RequestWrapper(httpRequest, pe), response);
return;
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,10 +67,10 @@ public abstract class AbstractAuthTests extends AbstractTestClass {
private static final List<Entity> DATASTREAMS = new ArrayList<>();
private static final List<Entity> OBSERVATIONS = new ArrayList<>();

private static SensorThingsService serviceAdmin;
private static SensorThingsService serviceWrite;
private static SensorThingsService serviceRead;
private static SensorThingsService serviceAnon;
protected static SensorThingsService serviceAdmin;
protected static SensorThingsService serviceWrite;
protected static SensorThingsService serviceRead;
protected static SensorThingsService serviceAnon;

private final boolean anonymousReadAllowed;
private final AuthTestHelper ath;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,26 @@
*/
package de.fraunhofer.iosb.ilt.statests.f01auth;

import static de.fraunhofer.iosb.ilt.statests.TestSuite.KEY_DB_NAME;
import static de.fraunhofer.iosb.ilt.statests.util.EntityUtils.filterForException;
import static de.fraunhofer.iosb.ilt.statests.util.EntityUtils.testFilterResults;

import dasniko.testcontainers.keycloak.KeycloakContainer;
import de.fraunhofer.iosb.ilt.frostclient.SensorThingsService;
import de.fraunhofer.iosb.ilt.frostclient.model.Entity;
import de.fraunhofer.iosb.ilt.frostclient.models.SensorThingsSensingV11;
import de.fraunhofer.iosb.ilt.frostclient.utils.TokenManagerOpenIDConnect;
import de.fraunhofer.iosb.ilt.statests.ServerVersion;
import de.fraunhofer.iosb.ilt.statests.TestSuite;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import org.apache.commons.io.IOUtils;
import org.junit.jupiter.api.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

Expand All @@ -45,11 +58,47 @@ public abstract class KeyCloakTests extends AbstractAuthTests {
public static final String KEYCLOAK_FROST_CONFIG_SECRET = "5aa9087d-817f-47b6-92a1-2b5f7caac967";
public static final String KEYCLOAK_TOKEN_PATH = "/realms/FROST-Test/protocol/openid-connect/token";

private static final SensorThingsSensingV11 mdlSensing = new SensorThingsSensingV11();
private static final SensorThingsUserModel mdlUsers = new SensorThingsUserModel();
private static final SensorThingsService baseService = new SensorThingsService(mdlSensing, mdlUsers);
private static final List<Entity> USERS = new ArrayList<>();

private static String modelUrl(String name) {
return resourceUrl("finegrainedsecurity/model/", name);
}

private static String resourceUrl(String path, String name) {
try {
return IOUtils.resourceToURL(path + "/" + name, KeyCloakTests.class.getClassLoader()).getFile();
} catch (IOException ex) {
LOGGER.error("Failed", ex);
return "";
}
}

static {
final String dbName = "keycloakauth";
SERVER_PROPERTIES.put("auth.db.url", TestSuite.createDbUrl(dbName));
SERVER_PROPERTIES.put("auth.db.driver", "org.postgresql.Driver");
SERVER_PROPERTIES.put("auth.db.username", TestSuite.VAL_PG_USER);
SERVER_PROPERTIES.put("auth.db.password", TestSuite.VAL_PG_PASS);
SERVER_PROPERTIES.put(KEY_DB_NAME, dbName);

SERVER_PROPERTIES.put("auth_provider", "de.fraunhofer.iosb.ilt.frostserver.auth.keycloak.KeycloakAuthProvider");
SERVER_PROPERTIES.put("auth_keycloakConfigUrl", TestSuite.getInstance().getKeycloak().getAuthServerUrl() + "/realms/FROST-Test/clients-registrations/install/" + KEYCLOAK_FROST_CLIENT_ID);
SERVER_PROPERTIES.put("auth_keycloakConfigSecret", KEYCLOAK_FROST_CONFIG_SECRET);
SERVER_PROPERTIES.put("auth_allowAnonymousRead", "false");
SERVER_PROPERTIES.put("auth_registerUserLocally", "true");
SERVER_PROPERTIES.put("plugins.coreModel.idType", "LONG");
SERVER_PROPERTIES.put("plugins.modelLoader.enable", "true");
SERVER_PROPERTIES.put("plugins.modelLoader.modelPath", "");
SERVER_PROPERTIES.put("plugins.modelLoader.modelFiles", modelUrl("Role.json") + ", " + modelUrl("UserNoPass.json"));
SERVER_PROPERTIES.put("plugins.modelLoader.liquibasePath", "target/test-classes/finegrainedsecurity/liquibase");
SERVER_PROPERTIES.put("plugins.modelLoader.liquibaseFiles", "tablesSecurityUPR.xml");
SERVER_PROPERTIES.put("plugins.modelLoader.idType.Role", "STRING");
SERVER_PROPERTIES.put("plugins.modelLoader.idType.User", "STRING");
SERVER_PROPERTIES.put("persistence.idGenerationMode.Role", "ClientGeneratedOnly");
SERVER_PROPERTIES.put("persistence.idGenerationMode.User", "ClientGeneratedOnly");
}

public KeyCloakTests(ServerVersion version) {
Expand All @@ -59,7 +108,29 @@ public KeyCloakTests(ServerVersion version) {
@Override
protected void setUpVersion() {
LOGGER.info("Setting up for version {}.", version.urlPart);
sMdl = mdlSensing;
super.setUpVersion();
USERS.clear();
USERS.add(mdlUsers.newUser("c8e84639-9914-4b1e-b756-349afed255f6", null));
USERS.add(mdlUsers.newUser("1d6b3bb2-a869-4686-b781-c1ea481e6085", null));
USERS.add(mdlUsers.newUser("74fe01f1-2ecc-4696-87f0-340ee3fe1a86", null));
}

@Test
void test_100_ReadUser() {
LOGGER.info(" test_100_ReadUser");
testFilterResults(serviceAdmin, mdlUsers.etUser, "", USERS);
filterForException(serviceAnon, mdlUsers.etUser, "", AuthTestHelper.HTTP_CODE_403_FORBIDDEN);
}

@Override
protected SensorThingsService createService() {
try {
return new SensorThingsService(baseService.getModelRegistry())
.setEndpoint(new URL(serverSettings.getServiceUrl(version)));
} catch (MalformedURLException ex) {
throw new IllegalArgumentException("Serversettings contains malformed URL.", ex);
}
}

@Override
Expand Down
Loading

0 comments on commit bb4b451

Please sign in to comment.