Skip to content

Commit bb4b451

Browse files
committed
Added option for KeycloakAuthProvider to locally register user
1 parent 69e7b66 commit bb4b451

10 files changed

Lines changed: 364 additions & 20 deletions

File tree

FROST-Server.Auth.Basic/src/main/resources/liquibase/basicAuthTables.xml

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,20 @@
3434
</createTable>
3535
</changeSet>
3636

37+
<changeSet author="scf" id="20231117-users" objectQuotingStrategy="QUOTE_ALL_OBJECTS">
38+
<!-- If there is a users table, but it has no password column. -->
39+
<preConditions onFail="MARK_RAN">
40+
<tableExists tableName="USERS" />
41+
<not>
42+
<columnExists tableName="USERS" columnName="USER_PASS" />
43+
</not>
44+
</preConditions>
45+
<addColumn tableName="USERS">
46+
<column name="USER_PASS" type="VARCHAR(255)" />
47+
</addColumn>
48+
</changeSet>
49+
50+
3751
<changeSet author="scf" id="20181121-user_roles" objectQuotingStrategy="QUOTE_ALL_OBJECTS">
3852
<preConditions onFail="MARK_RAN">
3953
<not>

FROST-Server.Auth.Keycloak/pom.xml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,11 @@
2525
<artifactId>FROST-Server.Core</artifactId>
2626
<version>${project.version}</version>
2727
</dependency>
28+
<dependency>
29+
<groupId>${project.groupId}</groupId>
30+
<artifactId>FROST-Server.SQLjooq</artifactId>
31+
<version>${project.version}</version>
32+
</dependency>
2833
<dependency>
2934
<groupId>${project.groupId}</groupId>
3035
<artifactId>FROST-Server.Util</artifactId>
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
/*
2+
* Copyright (C) 2023 Fraunhofer Institut IOSB, Fraunhoferstr. 1, D 76131
3+
* Karlsruhe, Germany.
4+
*
5+
* This program is free software: you can redistribute it and/or modify
6+
* it under the terms of the GNU Lesser General Public License as published by
7+
* the Free Software Foundation, either version 3 of the License, or
8+
* (at your option) any later version.
9+
*
10+
* This program is distributed in the hope that it will be useful,
11+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
12+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13+
* GNU Lesser General Public License for more details.
14+
*
15+
* You should have received a copy of the GNU Lesser General Public License
16+
* along with this program. If not, see <http://www.gnu.org/licenses/>.
17+
*/
18+
package de.fraunhofer.iosb.ilt.frostserver.auth.keycloak;
19+
20+
import static de.fraunhofer.iosb.ilt.frostserver.auth.keycloak.KeycloakAuthProvider.TAG_USERNAME_COLUMN;
21+
import static de.fraunhofer.iosb.ilt.frostserver.auth.keycloak.KeycloakAuthProvider.TAG_USER_TABLE;
22+
import static de.fraunhofer.iosb.ilt.frostserver.persistence.pgjooq.utils.ConnectionUtils.TAG_DB_URL;
23+
24+
import de.fraunhofer.iosb.ilt.frostserver.persistence.pgjooq.utils.ConnectionUtils;
25+
import de.fraunhofer.iosb.ilt.frostserver.persistence.pgjooq.utils.ConnectionUtils.ConnectionWrapper;
26+
import de.fraunhofer.iosb.ilt.frostserver.settings.CoreSettings;
27+
import de.fraunhofer.iosb.ilt.frostserver.settings.Settings;
28+
import java.sql.SQLException;
29+
import java.util.HashMap;
30+
import java.util.Map;
31+
import org.jooq.DSLContext;
32+
import org.jooq.Field;
33+
import org.jooq.Record;
34+
import org.jooq.SQLDialect;
35+
import org.jooq.Table;
36+
import org.jooq.impl.DSL;
37+
import org.slf4j.Logger;
38+
import org.slf4j.LoggerFactory;
39+
40+
/**
41+
*
42+
* @author scf
43+
*/
44+
public class DatabaseHandler {
45+
46+
/**
47+
* The logger for this class.
48+
*/
49+
private static final Logger LOGGER = LoggerFactory.getLogger(DatabaseHandler.class);
50+
51+
private static final Map<CoreSettings, DatabaseHandler> INSTANCES = new HashMap<>();
52+
53+
private final Settings authSettings;
54+
private final String connectionUrl;
55+
private final String userTable;
56+
private final String usernameColumn;
57+
58+
public static void init(CoreSettings coreSettings) {
59+
if (INSTANCES.get(coreSettings) == null) {
60+
createInstance(coreSettings);
61+
}
62+
}
63+
64+
private static synchronized DatabaseHandler createInstance(CoreSettings coreSettings) {
65+
return INSTANCES.computeIfAbsent(coreSettings, (s) -> {
66+
LOGGER.info("Initialising DatabaseHandler.");
67+
return new DatabaseHandler(coreSettings);
68+
});
69+
}
70+
71+
public static DatabaseHandler getInstance(CoreSettings coreSettings) {
72+
DatabaseHandler instance = INSTANCES.get(coreSettings);
73+
if (instance == null) {
74+
LOGGER.error("DatabaseHandler not initialised.");
75+
}
76+
return instance;
77+
}
78+
79+
private DatabaseHandler(CoreSettings coreSettings) {
80+
authSettings = coreSettings.getAuthSettings();
81+
connectionUrl = authSettings.get(TAG_DB_URL, ConnectionUtils.class, false);
82+
userTable = authSettings.get(TAG_USER_TABLE, KeycloakAuthProvider.class);
83+
usernameColumn = authSettings.get(TAG_USERNAME_COLUMN, KeycloakAuthProvider.class);
84+
}
85+
86+
/**
87+
* Checks if the user is registered locally and if not, add the user.
88+
*
89+
* @param username the username
90+
*/
91+
public void enureUserInUsertable(String username) {
92+
try (final ConnectionWrapper connectionProvider = new ConnectionWrapper(authSettings, connectionUrl)) {
93+
final DSLContext dslContext = DSL.using(connectionProvider.get(), SQLDialect.POSTGRES);
94+
final Field<String> usernameField = DSL.field(DSL.name(usernameColumn), String.class);
95+
final Table<Record> table = DSL.table(DSL.name(userTable));
96+
long count = dslContext
97+
.selectCount()
98+
.from(table)
99+
.where(usernameField.eq(username))
100+
.fetchOne()
101+
.component1();
102+
if (count == 0) {
103+
dslContext.insertInto(table)
104+
.set(usernameField, username)
105+
.execute();
106+
connectionProvider.commit();
107+
}
108+
} catch (SQLException | RuntimeException exc) {
109+
LOGGER.error("Failed to register user locally.", exc);
110+
}
111+
}
112+
113+
}

FROST-Server.Auth.Keycloak/src/main/java/de/fraunhofer/iosb/ilt/frostserver/auth/keycloak/KeycloakAuthProvider.java

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
import de.fraunhofer.iosb.ilt.frostserver.settings.CoreSettings;
2424
import de.fraunhofer.iosb.ilt.frostserver.settings.Settings;
2525
import de.fraunhofer.iosb.ilt.frostserver.settings.annotation.DefaultValue;
26+
import de.fraunhofer.iosb.ilt.frostserver.settings.annotation.DefaultValueBoolean;
2627
import de.fraunhofer.iosb.ilt.frostserver.settings.annotation.DefaultValueInt;
2728
import de.fraunhofer.iosb.ilt.frostserver.util.AuthProvider;
2829
import de.fraunhofer.iosb.ilt.frostserver.util.LiquibaseUser;
@@ -79,6 +80,15 @@ public class KeycloakAuthProvider implements AuthProvider, LiquibaseUser, Config
7980
@DefaultValueInt(10)
8081
public static final String TAG_MAX_CLIENTS_PER_USER = "maxClientsPerUser";
8182

83+
@DefaultValueBoolean(false)
84+
public static final String TAG_REGISTER_USER_LOCALLY = "registerUserLocally";
85+
86+
@DefaultValue("USERS")
87+
public static final String TAG_USER_TABLE = "userTable";
88+
89+
@DefaultValue("USER_NAME")
90+
public static final String TAG_USERNAME_COLUMN = "usernameColumn";
91+
8292
/**
8393
* The logger for this class.
8494
*/
@@ -95,6 +105,8 @@ public class KeycloakAuthProvider implements AuthProvider, LiquibaseUser, Config
95105
private CoreSettings coreSettings;
96106
private String roleAdmin;
97107
private int maxClientsPerUser;
108+
private boolean registerUserLocally;
109+
private DatabaseHandler databaseHandler;
98110

99111
private final Map<String, UserClientInfo> clientidToUserinfo = new ConcurrentHashMap<>();
100112
private final Map<String, UserClientInfo> usernameToUserinfo = new ConcurrentHashMap<>();
@@ -113,6 +125,11 @@ public void init(CoreSettings coreSettings) {
113125
final Settings authSettings = coreSettings.getAuthSettings();
114126
roleAdmin = authSettings.get(TAG_AUTH_ROLE_ADMIN, CoreSettings.class);
115127
maxClientsPerUser = authSettings.getInt(TAG_MAX_CLIENTS_PER_USER, getClass());
128+
registerUserLocally = authSettings.getBoolean(TAG_REGISTER_USER_LOCALLY, KeycloakAuthProvider.class);
129+
if (registerUserLocally) {
130+
DatabaseHandler.init(coreSettings);
131+
databaseHandler = DatabaseHandler.getInstance(coreSettings);
132+
}
116133
}
117134

118135
@Override
@@ -173,6 +190,9 @@ private boolean checkLogin(AbstractKeycloakLoginModule loginModule, UserData use
173190
client.setSubject(subject);
174191
CLIENTMAP.put(clientId, client);
175192
client.getSubject().getPrincipals().stream().forEach(t -> userData.roles.add(t.getName()));
193+
if (registerUserLocally) {
194+
databaseHandler.enureUserInUsertable(userData.userName);
195+
}
176196
}
177197
return login;
178198
} catch (LoginException ex) {

FROST-Server.Auth.Keycloak/src/main/java/de/fraunhofer/iosb/ilt/frostserver/auth/keycloak/KeycloakFilter.java

Lines changed: 18 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
*/
1818
package de.fraunhofer.iosb.ilt.frostserver.auth.keycloak;
1919

20+
import static de.fraunhofer.iosb.ilt.frostserver.auth.keycloak.KeycloakAuthProvider.TAG_REGISTER_USER_LOCALLY;
2021
import static de.fraunhofer.iosb.ilt.frostserver.settings.CoreSettings.TAG_AUTHENTICATE_ONLY;
2122
import static de.fraunhofer.iosb.ilt.frostserver.settings.CoreSettings.TAG_AUTH_ALLOW_ANON_READ;
2223
import static de.fraunhofer.iosb.ilt.frostserver.settings.CoreSettings.TAG_AUTH_ROLE_ADMIN;
@@ -43,7 +44,6 @@
4344
import javax.servlet.ServletRequest;
4445
import javax.servlet.ServletResponse;
4546
import javax.servlet.http.HttpServletRequest;
46-
import javax.servlet.http.HttpServletRequestWrapper;
4747
import javax.servlet.http.HttpServletResponse;
4848
import javax.servlet.http.HttpSession;
4949
import org.keycloak.adapters.AdapterDeploymentContext;
@@ -79,6 +79,8 @@ public class KeycloakFilter implements Filter {
7979
private Map<Role, String> roleMappings;
8080
private String roleAdmin;
8181
private boolean authenticateOnly;
82+
private boolean registerUserLocally;
83+
private DatabaseHandler databaseHandler;
8284

8385
private AdapterDeploymentContext deploymentContext;
8486
private NodesRegistrationManagement nodesRegistrationManagement;
@@ -96,7 +98,11 @@ public void init(FilterConfig filterConfig) throws ServletException {
9698
Settings authSettings = coreSettings.getAuthSettings();
9799
roleMappings = AuthUtils.loadRoleMapping(authSettings);
98100
roleAdmin = authSettings.get(TAG_AUTH_ROLE_ADMIN, CoreSettings.class);
99-
authenticateOnly = "T".equals(authSettings.get(TAG_AUTHENTICATE_ONLY, "F"));
101+
authenticateOnly = authSettings.getBoolean(TAG_AUTHENTICATE_ONLY, CoreSettings.class);
102+
registerUserLocally = authSettings.getBoolean(TAG_REGISTER_USER_LOCALLY, KeycloakAuthProvider.class);
103+
if (registerUserLocally) {
104+
databaseHandler = DatabaseHandler.getInstance(coreSettings);
105+
}
100106

101107
final boolean anonRead = authSettings.getBoolean(TAG_AUTH_ALLOW_ANON_READ, CoreSettings.class);
102108
roleMappersByPath.put("/Data", method -> Role.ADMIN);
@@ -209,19 +215,21 @@ public void doFilter(ServletRequest request, ServletResponse response, FilterCha
209215
LOGGER.debug("Request handled by authentication actions.");
210216
return;
211217
} else {
218+
final KeycloakAccount account = findKeycloakAccount(httpRequest);
219+
final Principal principalBasic = account.getPrincipal();
220+
final Set<String> roles = account.getRoles();
221+
final String userName = principalBasic.getName();
222+
final PrincipalExtended pe = new PrincipalExtended(userName, roles.contains(roleAdmin), roles);
223+
if (registerUserLocally) {
224+
databaseHandler.enureUserInUsertable(userName);
225+
}
212226
if (authenticateOnly) {
213-
final KeycloakAccount account = findKeycloakAccount(httpRequest);
214-
final Principal principalBasic = account.getPrincipal();
215-
final Set<String> roles = account.getRoles();
216-
final PrincipalExtended pe = new PrincipalExtended(principalBasic.getName(), roles.contains(roleAdmin), roles);
217227
chain.doFilter(new RequestWrapper(httpRequest, pe), response);
218228
return;
219229
}
220-
221-
HttpServletRequestWrapper wrapper = tokenStore.buildWrapper();
222-
if (wrapper.isUserInRole(roleMappings.get(requiredRole))) {
230+
if (roles.contains(roleMappings.get(requiredRole))) {
223231
LOGGER.debug("User has correct role.");
224-
chain.doFilter(wrapper, response);
232+
chain.doFilter(new RequestWrapper(httpRequest, pe), response);
225233
return;
226234
}
227235
}

FROST-Server.Tests/src/test/java/de/fraunhofer/iosb/ilt/statests/f01auth/AbstractAuthTests.java

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -67,10 +67,10 @@ public abstract class AbstractAuthTests extends AbstractTestClass {
6767
private static final List<Entity> DATASTREAMS = new ArrayList<>();
6868
private static final List<Entity> OBSERVATIONS = new ArrayList<>();
6969

70-
private static SensorThingsService serviceAdmin;
71-
private static SensorThingsService serviceWrite;
72-
private static SensorThingsService serviceRead;
73-
private static SensorThingsService serviceAnon;
70+
protected static SensorThingsService serviceAdmin;
71+
protected static SensorThingsService serviceWrite;
72+
protected static SensorThingsService serviceRead;
73+
protected static SensorThingsService serviceAnon;
7474

7575
private final boolean anonymousReadAllowed;
7676
private final AuthTestHelper ath;

FROST-Server.Tests/src/test/java/de/fraunhofer/iosb/ilt/statests/f01auth/KeyCloakTests.java

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,26 @@
1717
*/
1818
package de.fraunhofer.iosb.ilt.statests.f01auth;
1919

20+
import static de.fraunhofer.iosb.ilt.statests.TestSuite.KEY_DB_NAME;
21+
import static de.fraunhofer.iosb.ilt.statests.util.EntityUtils.filterForException;
22+
import static de.fraunhofer.iosb.ilt.statests.util.EntityUtils.testFilterResults;
23+
2024
import dasniko.testcontainers.keycloak.KeycloakContainer;
2125
import de.fraunhofer.iosb.ilt.frostclient.SensorThingsService;
26+
import de.fraunhofer.iosb.ilt.frostclient.model.Entity;
27+
import de.fraunhofer.iosb.ilt.frostclient.models.SensorThingsSensingV11;
2228
import de.fraunhofer.iosb.ilt.frostclient.utils.TokenManagerOpenIDConnect;
2329
import de.fraunhofer.iosb.ilt.statests.ServerVersion;
2430
import de.fraunhofer.iosb.ilt.statests.TestSuite;
31+
import java.io.IOException;
32+
import java.net.MalformedURLException;
33+
import java.net.URL;
34+
import java.util.ArrayList;
2535
import java.util.LinkedHashMap;
36+
import java.util.List;
2637
import java.util.Map;
38+
import org.apache.commons.io.IOUtils;
39+
import org.junit.jupiter.api.Test;
2740
import org.slf4j.Logger;
2841
import org.slf4j.LoggerFactory;
2942

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

61+
private static final SensorThingsSensingV11 mdlSensing = new SensorThingsSensingV11();
62+
private static final SensorThingsUserModel mdlUsers = new SensorThingsUserModel();
63+
private static final SensorThingsService baseService = new SensorThingsService(mdlSensing, mdlUsers);
64+
private static final List<Entity> USERS = new ArrayList<>();
65+
66+
private static String modelUrl(String name) {
67+
return resourceUrl("finegrainedsecurity/model/", name);
68+
}
69+
70+
private static String resourceUrl(String path, String name) {
71+
try {
72+
return IOUtils.resourceToURL(path + "/" + name, KeyCloakTests.class.getClassLoader()).getFile();
73+
} catch (IOException ex) {
74+
LOGGER.error("Failed", ex);
75+
return "";
76+
}
77+
}
78+
4879
static {
80+
final String dbName = "keycloakauth";
81+
SERVER_PROPERTIES.put("auth.db.url", TestSuite.createDbUrl(dbName));
82+
SERVER_PROPERTIES.put("auth.db.driver", "org.postgresql.Driver");
83+
SERVER_PROPERTIES.put("auth.db.username", TestSuite.VAL_PG_USER);
84+
SERVER_PROPERTIES.put("auth.db.password", TestSuite.VAL_PG_PASS);
85+
SERVER_PROPERTIES.put(KEY_DB_NAME, dbName);
86+
4987
SERVER_PROPERTIES.put("auth_provider", "de.fraunhofer.iosb.ilt.frostserver.auth.keycloak.KeycloakAuthProvider");
5088
SERVER_PROPERTIES.put("auth_keycloakConfigUrl", TestSuite.getInstance().getKeycloak().getAuthServerUrl() + "/realms/FROST-Test/clients-registrations/install/" + KEYCLOAK_FROST_CLIENT_ID);
5189
SERVER_PROPERTIES.put("auth_keycloakConfigSecret", KEYCLOAK_FROST_CONFIG_SECRET);
5290
SERVER_PROPERTIES.put("auth_allowAnonymousRead", "false");
91+
SERVER_PROPERTIES.put("auth_registerUserLocally", "true");
92+
SERVER_PROPERTIES.put("plugins.coreModel.idType", "LONG");
93+
SERVER_PROPERTIES.put("plugins.modelLoader.enable", "true");
94+
SERVER_PROPERTIES.put("plugins.modelLoader.modelPath", "");
95+
SERVER_PROPERTIES.put("plugins.modelLoader.modelFiles", modelUrl("Role.json") + ", " + modelUrl("UserNoPass.json"));
96+
SERVER_PROPERTIES.put("plugins.modelLoader.liquibasePath", "target/test-classes/finegrainedsecurity/liquibase");
97+
SERVER_PROPERTIES.put("plugins.modelLoader.liquibaseFiles", "tablesSecurityUPR.xml");
98+
SERVER_PROPERTIES.put("plugins.modelLoader.idType.Role", "STRING");
99+
SERVER_PROPERTIES.put("plugins.modelLoader.idType.User", "STRING");
100+
SERVER_PROPERTIES.put("persistence.idGenerationMode.Role", "ClientGeneratedOnly");
101+
SERVER_PROPERTIES.put("persistence.idGenerationMode.User", "ClientGeneratedOnly");
53102
}
54103

55104
public KeyCloakTests(ServerVersion version) {
@@ -59,7 +108,29 @@ public KeyCloakTests(ServerVersion version) {
59108
@Override
60109
protected void setUpVersion() {
61110
LOGGER.info("Setting up for version {}.", version.urlPart);
111+
sMdl = mdlSensing;
62112
super.setUpVersion();
113+
USERS.clear();
114+
USERS.add(mdlUsers.newUser("c8e84639-9914-4b1e-b756-349afed255f6", null));
115+
USERS.add(mdlUsers.newUser("1d6b3bb2-a869-4686-b781-c1ea481e6085", null));
116+
USERS.add(mdlUsers.newUser("74fe01f1-2ecc-4696-87f0-340ee3fe1a86", null));
117+
}
118+
119+
@Test
120+
void test_100_ReadUser() {
121+
LOGGER.info(" test_100_ReadUser");
122+
testFilterResults(serviceAdmin, mdlUsers.etUser, "", USERS);
123+
filterForException(serviceAnon, mdlUsers.etUser, "", AuthTestHelper.HTTP_CODE_403_FORBIDDEN);
124+
}
125+
126+
@Override
127+
protected SensorThingsService createService() {
128+
try {
129+
return new SensorThingsService(baseService.getModelRegistry())
130+
.setEndpoint(new URL(serverSettings.getServiceUrl(version)));
131+
} catch (MalformedURLException ex) {
132+
throw new IllegalArgumentException("Serversettings contains malformed URL.", ex);
133+
}
63134
}
64135

65136
@Override

0 commit comments

Comments
 (0)