diff --git a/FROST-Server.Auth.Basic/src/main/resources/liquibase/basicAuthTables.xml b/FROST-Server.Auth.Basic/src/main/resources/liquibase/basicAuthTables.xml index 22b82f852..a3d58a061 100644 --- a/FROST-Server.Auth.Basic/src/main/resources/liquibase/basicAuthTables.xml +++ b/FROST-Server.Auth.Basic/src/main/resources/liquibase/basicAuthTables.xml @@ -34,6 +34,20 @@ + + + + + + + + + + + + + + diff --git a/FROST-Server.Auth.Keycloak/pom.xml b/FROST-Server.Auth.Keycloak/pom.xml index 1423b03d7..09527ea50 100644 --- a/FROST-Server.Auth.Keycloak/pom.xml +++ b/FROST-Server.Auth.Keycloak/pom.xml @@ -25,6 +25,11 @@ FROST-Server.Core ${project.version} + + ${project.groupId} + FROST-Server.SQLjooq + ${project.version} + ${project.groupId} FROST-Server.Util diff --git a/FROST-Server.Auth.Keycloak/src/main/java/de/fraunhofer/iosb/ilt/frostserver/auth/keycloak/DatabaseHandler.java b/FROST-Server.Auth.Keycloak/src/main/java/de/fraunhofer/iosb/ilt/frostserver/auth/keycloak/DatabaseHandler.java new file mode 100644 index 000000000..b3601148a --- /dev/null +++ b/FROST-Server.Auth.Keycloak/src/main/java/de/fraunhofer/iosb/ilt/frostserver/auth/keycloak/DatabaseHandler.java @@ -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 . + */ +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 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 usernameField = DSL.field(DSL.name(usernameColumn), String.class); + final Table 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); + } + } + +} diff --git a/FROST-Server.Auth.Keycloak/src/main/java/de/fraunhofer/iosb/ilt/frostserver/auth/keycloak/KeycloakAuthProvider.java b/FROST-Server.Auth.Keycloak/src/main/java/de/fraunhofer/iosb/ilt/frostserver/auth/keycloak/KeycloakAuthProvider.java index 090ac7e94..fc854d00c 100644 --- a/FROST-Server.Auth.Keycloak/src/main/java/de/fraunhofer/iosb/ilt/frostserver/auth/keycloak/KeycloakAuthProvider.java +++ b/FROST-Server.Auth.Keycloak/src/main/java/de/fraunhofer/iosb/ilt/frostserver/auth/keycloak/KeycloakAuthProvider.java @@ -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; @@ -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. */ @@ -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 clientidToUserinfo = new ConcurrentHashMap<>(); private final Map usernameToUserinfo = new ConcurrentHashMap<>(); @@ -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 @@ -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) { diff --git a/FROST-Server.Auth.Keycloak/src/main/java/de/fraunhofer/iosb/ilt/frostserver/auth/keycloak/KeycloakFilter.java b/FROST-Server.Auth.Keycloak/src/main/java/de/fraunhofer/iosb/ilt/frostserver/auth/keycloak/KeycloakFilter.java index bee289f8c..ae8591061 100644 --- a/FROST-Server.Auth.Keycloak/src/main/java/de/fraunhofer/iosb/ilt/frostserver/auth/keycloak/KeycloakFilter.java +++ b/FROST-Server.Auth.Keycloak/src/main/java/de/fraunhofer/iosb/ilt/frostserver/auth/keycloak/KeycloakFilter.java @@ -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; @@ -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; @@ -79,6 +79,8 @@ public class KeycloakFilter implements Filter { private Map roleMappings; private String roleAdmin; private boolean authenticateOnly; + private boolean registerUserLocally; + private DatabaseHandler databaseHandler; private AdapterDeploymentContext deploymentContext; private NodesRegistrationManagement nodesRegistrationManagement; @@ -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); @@ -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 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 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; } } diff --git a/FROST-Server.Tests/src/test/java/de/fraunhofer/iosb/ilt/statests/f01auth/AbstractAuthTests.java b/FROST-Server.Tests/src/test/java/de/fraunhofer/iosb/ilt/statests/f01auth/AbstractAuthTests.java index 20976e6be..93e4d243c 100644 --- a/FROST-Server.Tests/src/test/java/de/fraunhofer/iosb/ilt/statests/f01auth/AbstractAuthTests.java +++ b/FROST-Server.Tests/src/test/java/de/fraunhofer/iosb/ilt/statests/f01auth/AbstractAuthTests.java @@ -67,10 +67,10 @@ public abstract class AbstractAuthTests extends AbstractTestClass { private static final List DATASTREAMS = new ArrayList<>(); private static final List 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; diff --git a/FROST-Server.Tests/src/test/java/de/fraunhofer/iosb/ilt/statests/f01auth/KeyCloakTests.java b/FROST-Server.Tests/src/test/java/de/fraunhofer/iosb/ilt/statests/f01auth/KeyCloakTests.java index d3bd822b6..6e81be074 100644 --- a/FROST-Server.Tests/src/test/java/de/fraunhofer/iosb/ilt/statests/f01auth/KeyCloakTests.java +++ b/FROST-Server.Tests/src/test/java/de/fraunhofer/iosb/ilt/statests/f01auth/KeyCloakTests.java @@ -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; @@ -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 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) { @@ -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 diff --git a/FROST-Server.Tests/src/test/java/de/fraunhofer/iosb/ilt/statests/util/EntityUtils.java b/FROST-Server.Tests/src/test/java/de/fraunhofer/iosb/ilt/statests/util/EntityUtils.java index e431f0911..ac1491d26 100644 --- a/FROST-Server.Tests/src/test/java/de/fraunhofer/iosb/ilt/statests/util/EntityUtils.java +++ b/FROST-Server.Tests/src/test/java/de/fraunhofer/iosb/ilt/statests/util/EntityUtils.java @@ -135,6 +135,10 @@ public static void deleteAll(SensorThingsService service) throws ServiceFailureE deleteAll(service.dao(mr.getEntityTypeForName("Thing"))); } for (de.fraunhofer.iosb.ilt.frostclient.model.EntityType et : mr.getEntityTypes()) { + if ("user".equalsIgnoreCase(et.entityName)) { + // Can't usually delete users. + continue; + } try { deleteAll(service.dao(et)); } catch (NotFoundException exc) { diff --git a/FROST-Server.Tests/src/test/resources/finegrainedsecurity/liquibase/tablesSecurityUPR.xml b/FROST-Server.Tests/src/test/resources/finegrainedsecurity/liquibase/tablesSecurityUPR.xml index 7e01102dc..1c52ce386 100644 --- a/FROST-Server.Tests/src/test/resources/finegrainedsecurity/liquibase/tablesSecurityUPR.xml +++ b/FROST-Server.Tests/src/test/resources/finegrainedsecurity/liquibase/tablesSecurityUPR.xml @@ -20,6 +20,38 @@ You should have received a copy of the GNU Lesser General Public License along with this program. If not, see . --> + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -46,6 +78,34 @@ onDelete="CASCADE" onUpdate="CASCADE" /> + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -64,8 +124,7 @@ - + @@ -77,8 +136,7 @@ - + - + diff --git a/FROST-Server.Tests/src/test/resources/finegrainedsecurity/model/UserNoPass.json b/FROST-Server.Tests/src/test/resources/finegrainedsecurity/model/UserNoPass.json new file mode 100644 index 000000000..65f95a367 --- /dev/null +++ b/FROST-Server.Tests/src/test/resources/finegrainedsecurity/model/UserNoPass.json @@ -0,0 +1,52 @@ +{ + "conformance": [], + "simplePropertyTypes": [], + "entityTypes": [ + { + "name": "User", + "plural": "Users", + "adminOnly": false, + "table": "USERS", + "entityProperties": [ + { + "name": "username", + "type": "Id", + "handlers": [ + { + "@class": "de.fraunhofer.iosb.ilt.frostserver.persistence.pgjooq.utils.fieldmapper.FieldMapperId", + "field": "USER_NAME" + } + ], + "annotations": [] + } + ], + "navigationProperties": [ + { + "name": "Roles", + "entitySet": true, + "entityType": "Role", + "required": false, + "inverse": { + "name": "Users", + "entitySet": true, + "required": false, + "annotations": [] + }, + "handlers": [ + { + "@class": "de.fraunhofer.iosb.ilt.frostserver.persistence.pgjooq.utils.fieldmapper.FieldMapperManyToMany", + "field": "USER_NAME", + "linkTable": "USER_ROLES", + "linkOurField": "USER_NAME", + "linkOtherField": "ROLE_NAME", + "otherTable": "ROLES", + "otherField": "ROLE_NAME" + } + ], + "annotations": [] + } + ], + "annotations": [] + } + ] +}