org.opennms.core.test-api
org.opennms.core.test-api.db
diff --git a/opennms-config/src/main/java/org/opennms/netmgt/config/DatabaseUserManager.java b/opennms-config/src/main/java/org/opennms/netmgt/config/DatabaseUserManager.java
new file mode 100644
index 000000000000..49c6c08261d3
--- /dev/null
+++ b/opennms-config/src/main/java/org/opennms/netmgt/config/DatabaseUserManager.java
@@ -0,0 +1,346 @@
+/*
+ * Licensed to The OpenNMS Group, Inc (TOG) under one or more
+ * contributor license agreements. See the LICENSE.md file
+ * distributed with this work for additional information
+ * regarding copyright ownership.
+ *
+ * TOG licenses this file to You under the GNU Affero General
+ * Public License Version 3 (the "License") or (at your option)
+ * any later version. You may not use this file except in
+ * compliance with the License. You may obtain a copy of the
+ * License at:
+ *
+ * https://www.gnu.org/licenses/agpl-3.0.txt
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
+ * either express or implied. See the License for the specific
+ * language governing permissions and limitations under the
+ * License.
+ */
+package org.opennms.netmgt.config;
+
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.StringReader;
+import java.sql.Connection;
+import java.sql.PreparedStatement;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.time.ZoneId;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.TreeMap;
+
+import javax.sql.DataSource;
+
+import org.opennms.core.db.DataSourceFactory;
+import org.opennms.core.xml.JaxbUtils;
+import org.opennms.netmgt.config.users.Contact;
+import org.opennms.netmgt.config.users.DutySchedule;
+import org.opennms.netmgt.config.users.Password;
+import org.opennms.netmgt.config.users.User;
+import org.opennms.netmgt.config.users.Userinfo;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Database-backed replacement for {@link UserFactory}.
+ *
+ * Reads and writes all user data from the {@code users} table (and child
+ * tables {@code user_roles}, {@code user_contacts}, {@code user_duty_schedules})
+ * created by Liquibase changeset 36.0.2. The {@code users.xml} file is not
+ * read or written.
+ *
+ * The in-memory cache inherited from {@link UserManager} is still used so
+ * that hot-path reads (duty-schedule checks, contact lookups) do not hit the
+ * database. The cache is populated once at startup via {@link #reload()} and
+ * is refreshed whenever {@link #saveXML} is called (i.e. after any write
+ * operation).
+ */
+public class DatabaseUserManager extends UserManager {
+
+ private static final Logger LOG = LoggerFactory.getLogger(DatabaseUserManager.class);
+
+ private static UserManager instance;
+ private static boolean initialized = false;
+
+ private volatile long m_lastModified = 0L;
+
+ protected DatabaseUserManager(final GroupManager groupManager) {
+ super(groupManager);
+ }
+
+ // -------------------------------------------------------------------------
+ // Singleton lifecycle (mirrors UserFactory pattern)
+ // -------------------------------------------------------------------------
+
+ public static synchronized void init() throws IOException, FileNotFoundException {
+ if (instance == null || !initialized) {
+ GroupFactory.init();
+ instance = new DatabaseUserManager(GroupFactory.getInstance());
+ instance.reload();
+ initialized = true;
+ // Register as the UserFactory singleton so legacy code that calls
+ // UserFactory.init() / UserFactory.getInstance() gets this instance.
+ // UserFactory.setInstance() sets UserFactory.initialized = true, which
+ // makes subsequent UserFactory.init() calls a no-op.
+ UserFactory.setInstance(instance);
+ }
+ }
+
+ public static synchronized UserManager getInstance() {
+ return instance;
+ }
+
+ public static synchronized void setInstance(final UserManager mgr) {
+ initialized = true;
+ instance = mgr;
+ }
+
+ // -------------------------------------------------------------------------
+ // UserManager abstract methods
+ // -------------------------------------------------------------------------
+
+ @Override
+ public synchronized void reload() throws IOException, FileNotFoundException {
+ loadFromDatabase();
+ m_lastModified = System.currentTimeMillis();
+ }
+
+ /** No-op: the database is always the authoritative source; no file to watch. */
+ @Override
+ public void doUpdate() throws IOException {
+ }
+
+ /** Always false: writes go directly to the DB via {@link #saveXML}. */
+ @Override
+ public boolean isUpdateNeeded() {
+ return false;
+ }
+
+ @Override
+ public long getLastModified() {
+ return m_lastModified;
+ }
+
+ /** Not meaningful for a database backend; returns 0. */
+ @Override
+ public long getFileSize() {
+ return 0;
+ }
+
+ /**
+ * Writes the current in-memory user map directly to the database, bypassing
+ * JAXB marshalling (which fails when the user list is empty due to XSD constraints).
+ */
+ @Override
+ protected void _saveCurrent() throws Exception {
+ final DataSource ds = DataSourceFactory.getInstance();
+ try (Connection conn = ds.getConnection()) {
+ final boolean prevAutoCommit = conn.getAutoCommit();
+ conn.setAutoCommit(false);
+ try {
+ try (PreparedStatement ps = conn.prepareStatement("DELETE FROM users")) {
+ ps.executeUpdate();
+ }
+ for (final User user : m_users.values()) {
+ insertUser(conn, user);
+ }
+ conn.commit();
+ m_lastModified = System.currentTimeMillis();
+ LOG.debug("Saved {} users to database", m_users.size());
+ } catch (final Exception e) {
+ conn.rollback();
+ throw e;
+ } finally {
+ conn.setAutoCommit(prevAutoCommit);
+ }
+ }
+ }
+
+ /**
+ * Receives a fully-serialized user list (from the REST API) and syncs it
+ * atomically to the database.
+ */
+ @Override
+ protected void saveXML(final String writerString) throws IOException {
+ if (writerString == null) return;
+ try {
+ final Userinfo userinfo = JaxbUtils.unmarshal(Userinfo.class, new StringReader(writerString));
+ final DataSource ds = DataSourceFactory.getInstance();
+ try (Connection conn = ds.getConnection()) {
+ final boolean prevAutoCommit = conn.getAutoCommit();
+ conn.setAutoCommit(false);
+ try {
+ // Replace all users atomically; child rows cascade on DELETE.
+ try (PreparedStatement ps = conn.prepareStatement("DELETE FROM users")) {
+ ps.executeUpdate();
+ }
+ for (final User user : userinfo.getUsers()) {
+ insertUser(conn, user);
+ }
+ conn.commit();
+ m_lastModified = System.currentTimeMillis();
+ LOG.debug("Saved {} users to database", userinfo.getUsers().size());
+ } catch (final Exception e) {
+ conn.rollback();
+ throw e;
+ } finally {
+ conn.setAutoCommit(prevAutoCommit);
+ }
+ }
+ } catch (final Exception e) {
+ throw new IOException("Failed to save users to database", e);
+ }
+ }
+
+ // -------------------------------------------------------------------------
+ // Database helpers
+ // -------------------------------------------------------------------------
+
+ private void loadFromDatabase() throws IOException {
+ final DataSource ds = DataSourceFactory.getInstance();
+ try (Connection conn = ds.getConnection()) {
+ final Map newUsers = new TreeMap<>();
+
+ // Core user fields
+ try (PreparedStatement ps = conn.prepareStatement(
+ "SELECT user_id, full_name, user_comments, password, password_salt, tui_pin, time_zone_id FROM users");
+ ResultSet rs = ps.executeQuery()) {
+ while (rs.next()) {
+ final User user = new User();
+ user.setUserId(rs.getString("user_id"));
+ final String fullName = rs.getString("full_name");
+ if (fullName != null) user.setFullName(fullName);
+ final String comments = rs.getString("user_comments");
+ if (comments != null) user.setUserComments(comments);
+ final Password pass = new Password();
+ pass.setEncryptedPassword(rs.getString("password"));
+ pass.setSalt(rs.getBoolean("password_salt"));
+ user.setPassword(pass);
+ final String tuiPin = rs.getString("tui_pin");
+ if (tuiPin != null) user.setTuiPin(tuiPin);
+ final String tzId = rs.getString("time_zone_id");
+ if (tzId != null) user.setTimeZoneId(ZoneId.of(tzId));
+ newUsers.put(user.getUserId(), user);
+ }
+ }
+
+ // Roles
+ try (PreparedStatement ps = conn.prepareStatement(
+ "SELECT user_id, role FROM user_roles ORDER BY user_id");
+ ResultSet rs = ps.executeQuery()) {
+ while (rs.next()) {
+ final User user = newUsers.get(rs.getString("user_id"));
+ if (user != null) user.addRole(rs.getString("role"));
+ }
+ }
+
+ // Contacts (email, pager, phone, xmpp, etc.)
+ try (PreparedStatement ps = conn.prepareStatement(
+ "SELECT user_id, contact_type, contact_info, service_provider FROM user_contacts ORDER BY user_id");
+ ResultSet rs = ps.executeQuery()) {
+ while (rs.next()) {
+ final User user = newUsers.get(rs.getString("user_id"));
+ if (user != null) {
+ final Contact contact = new Contact();
+ contact.setType(rs.getString("contact_type"));
+ final String info = rs.getString("contact_info");
+ if (info != null) contact.setInfo(info);
+ final String provider = rs.getString("service_provider");
+ if (provider != null) contact.setServiceProvider(provider);
+ user.addContact(contact);
+ }
+ }
+ }
+
+ // Duty schedules
+ try (PreparedStatement ps = conn.prepareStatement(
+ "SELECT user_id, schedule FROM user_duty_schedules ORDER BY user_id");
+ ResultSet rs = ps.executeQuery()) {
+ while (rs.next()) {
+ final User user = newUsers.get(rs.getString("user_id"));
+ if (user != null) user.addDutySchedule(rs.getString("schedule"));
+ }
+ }
+
+ m_users = newUsers;
+ rebuildDutySchedules();
+ LOG.debug("Loaded {} users from database", newUsers.size());
+
+ } catch (final SQLException e) {
+ throw new IOException("Failed to load users from database", e);
+ }
+ }
+
+ private void insertUser(final Connection conn, final User user) throws SQLException {
+ try (PreparedStatement ps = conn.prepareStatement(
+ "INSERT INTO users (user_id, full_name, user_comments, password, password_salt, tui_pin, time_zone_id)"
+ + " VALUES (?, ?, ?, ?, ?, ?, ?)")) {
+ ps.setString(1, user.getUserId());
+ ps.setString(2, user.getFullName().orElse(null));
+ ps.setString(3, user.getUserComments().orElse(null));
+ ps.setString(4, user.getPassword().getEncryptedPassword());
+ ps.setBoolean(5, user.getPassword().getSalt());
+ ps.setString(6, user.getTuiPin().orElse(null));
+ ps.setString(7, user.getTimeZoneId().map(ZoneId::getId).orElse(null));
+ ps.executeUpdate();
+ }
+
+ for (final String role : user.getRoles()) {
+ try (PreparedStatement ps = conn.prepareStatement(
+ "INSERT INTO user_roles (user_id, role) VALUES (?, ?)")) {
+ ps.setString(1, user.getUserId());
+ ps.setString(2, role);
+ ps.executeUpdate();
+ }
+ }
+
+ // Deduplicate contacts by type: last value wins, matching UserManager._setContact() behavior.
+ // The user_contacts table has a UNIQUE(user_id, contact_type) constraint.
+ final LinkedHashMap contactsByType = new LinkedHashMap<>();
+ for (final Contact contact : user.getContacts()) {
+ contactsByType.put(contact.getType(), contact);
+ }
+ for (final Contact contact : contactsByType.values()) {
+ try (PreparedStatement ps = conn.prepareStatement(
+ "INSERT INTO user_contacts (user_id, contact_type, contact_info, service_provider)"
+ + " VALUES (?, ?, ?, ?)")) {
+ ps.setString(1, user.getUserId());
+ ps.setString(2, contact.getType());
+ ps.setString(3, contact.getInfo().orElse(null));
+ ps.setString(4, contact.getServiceProvider().orElse(null));
+ ps.executeUpdate();
+ }
+ }
+
+ for (final String schedule : user.getDutySchedules()) {
+ try (PreparedStatement ps = conn.prepareStatement(
+ "INSERT INTO user_duty_schedules (user_id, schedule) VALUES (?, ?)")) {
+ ps.setString(1, user.getUserId());
+ ps.setString(2, schedule);
+ ps.executeUpdate();
+ }
+ }
+ }
+
+ // Replicates UserManager._buildDutySchedules() which is private.
+ private void rebuildDutySchedules() {
+ m_dutySchedules = new HashMap<>();
+ for (final Map.Entry entry : m_users.entrySet()) {
+ final User user = entry.getValue();
+ if (!user.getDutySchedules().isEmpty()) {
+ final List dutyList = new ArrayList<>();
+ for (final String duty : user.getDutySchedules()) {
+ dutyList.add(new DutySchedule(duty));
+ }
+ m_dutySchedules.put(entry.getKey(), dutyList);
+ }
+ }
+ }
+}
diff --git a/opennms-config/src/main/java/org/opennms/netmgt/config/UserFactory.java b/opennms-config/src/main/java/org/opennms/netmgt/config/UserFactory.java
index 70f07dc36690..036b6caaec0a 100644
--- a/opennms-config/src/main/java/org/opennms/netmgt/config/UserFactory.java
+++ b/opennms-config/src/main/java/org/opennms/netmgt/config/UserFactory.java
@@ -35,6 +35,8 @@
import org.apache.commons.io.IOUtils;
import org.opennms.core.utils.ConfigFileConstants;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
/**
* UserFactory class.
@@ -43,6 +45,7 @@
* @version $Id: $
*/
public class UserFactory extends UserManager {
+ private static final Logger LOG = LoggerFactory.getLogger(UserFactory.class);
private static final long RELOAD_CHECK_INTERVAL_MS = TimeUnit.SECONDS.toMillis(1);
/**
@@ -89,14 +92,22 @@ public UserFactory() throws FileNotFoundException, IOException {
* @throws java.io.IOException if any.
* @throws java.io.FileNotFoundException if any.
*/
- public static synchronized void init() throws IOException, FileNotFoundException {
-
- if (instance == null || !initialized) {
- GroupFactory.init();
- instance = new UserFactory();
+ public static synchronized void init() throws IOException {
+ if (instance != null && initialized) {
+ return;
+ }
+ GroupFactory.init();
+ final java.io.File usersConfFile;
+ try {
+ usersConfFile = ConfigFileConstants.getFile(ConfigFileConstants.USERS_CONF_FILE_NAME);
+ } catch (final FileNotFoundException e) {
+ // users.xml removed — DatabaseUserManager is authoritative.
initialized = true;
+ LOG.info("users.xml not found; skipping file-backed UserFactory init");
+ return;
}
-
+ instance = new UserFactory();
+ initialized = true;
}
/**
diff --git a/opennms-config/src/main/java/org/opennms/netmgt/config/UserManager.java b/opennms-config/src/main/java/org/opennms/netmgt/config/UserManager.java
index dd6c804b5e71..742622738e58 100644
--- a/opennms-config/src/main/java/org/opennms/netmgt/config/UserManager.java
+++ b/opennms-config/src/main/java/org/opennms/netmgt/config/UserManager.java
@@ -931,6 +931,7 @@ public void saveUsers(final Collection usersList) throws Exception {
for (final User curUser : usersList) {
m_users.put(curUser.getUserId(), curUser);
}
+ _saveCurrent();
} finally {
m_writeLock.unlock();
}
@@ -973,7 +974,7 @@ public void deleteUser(final String name) throws Exception {
/**
* Saves into "users.xml" file
*/
- private void _saveCurrent() throws Exception {
+ protected void _saveCurrent() throws Exception {
final List users = new ArrayList<>(m_users.values());
final Userinfo userinfo = new Userinfo();
diff --git a/opennms-config/src/main/java/org/opennms/netmgt/config/WebRoleManagerImpl.java b/opennms-config/src/main/java/org/opennms/netmgt/config/WebRoleManagerImpl.java
index 99d865fa3070..a566ee64ba76 100644
--- a/opennms-config/src/main/java/org/opennms/netmgt/config/WebRoleManagerImpl.java
+++ b/opennms-config/src/main/java/org/opennms/netmgt/config/WebRoleManagerImpl.java
@@ -79,7 +79,7 @@ private User getBackingUser(String name) {
try {
return m_userManager.getUser(name);
} catch (IOException e) {
- throw new WebRolesException("Error reading users.xml config file", e);
+ throw new WebRolesException("Error reading user from database", e);
}
}
@@ -87,7 +87,7 @@ private Collection getBackingUsers() {
try {
return m_userManager.getUsers().values();
} catch (IOException e) {
- throw new WebRolesException("Error reading users.xml config file", e);
+ throw new WebRolesException("Error reading user from database", e);
}
}
@@ -121,7 +121,7 @@ private Collection getUsersScheduleForRole(WebRole role, Date time) {
}
return webUsers;
} catch (IOException e) {
- throw new WebRolesException("Error reading users.xml config file", e);
+ throw new WebRolesException("Error reading user from database", e);
}
}
diff --git a/opennms-config/src/main/resources/META-INF/opennms/applicationContext-commonConfigs.xml b/opennms-config/src/main/resources/META-INF/opennms/applicationContext-commonConfigs.xml
index 1a90e0dba20e..95cd515797c3 100644
--- a/opennms-config/src/main/resources/META-INF/opennms/applicationContext-commonConfigs.xml
+++ b/opennms-config/src/main/resources/META-INF/opennms/applicationContext-commonConfigs.xml
@@ -67,10 +67,10 @@
- org.opennms.netmgt.config.UserFactory.init
+ org.opennms.netmgt.config.DatabaseUserManager.init
-
+
diff --git a/opennms-config/src/test/java/org/opennms/netmgt/config/DatabaseUserManagerTest.java b/opennms-config/src/test/java/org/opennms/netmgt/config/DatabaseUserManagerTest.java
new file mode 100644
index 000000000000..c56a4f19ae0d
--- /dev/null
+++ b/opennms-config/src/test/java/org/opennms/netmgt/config/DatabaseUserManagerTest.java
@@ -0,0 +1,340 @@
+/*
+ * Licensed to The OpenNMS Group, Inc (TOG) under one or more
+ * contributor license agreements. See the LICENSE.md file
+ * distributed with this work for additional information
+ * regarding copyright ownership.
+ *
+ * TOG licenses this file to You under the GNU Affero General
+ * Public License Version 3 (the "License") or (at your option)
+ * any later version. You may not use this file except in
+ * compliance with the License. You may obtain a copy of the
+ * License at:
+ *
+ * https://www.gnu.org/licenses/agpl-3.0.txt
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
+ * either express or implied. See the License for the specific
+ * language governing permissions and limitations under the
+ * License.
+ */
+package org.opennms.netmgt.config;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Mockito.mock;
+
+import java.io.File;
+import java.io.FileWriter;
+import java.nio.file.Files;
+import java.sql.Connection;
+import java.sql.DriverManager;
+import java.sql.ResultSet;
+import java.sql.Statement;
+import java.util.Arrays;
+import java.util.Map;
+
+import javax.sql.DataSource;
+
+import org.h2.jdbcx.JdbcDataSource;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.opennms.core.db.DataSourceFactory;
+import org.opennms.netmgt.config.users.Contact;
+import org.opennms.netmgt.config.users.Password;
+import org.opennms.netmgt.config.users.User;
+
+public class DatabaseUserManagerTest {
+
+ private Connection conn;
+ private DatabaseUserManager manager;
+
+ @Before
+ public void setUp() throws Exception {
+ Class.forName("org.h2.Driver");
+ // Each test gets its own named in-memory DB so tests don't bleed into each other
+ final String dbName = "users_test_" + Thread.currentThread().getId();
+ conn = DriverManager.getConnection("jdbc:h2:mem:" + dbName + ";DB_CLOSE_DELAY=-1");
+ createSchema(conn);
+
+ final JdbcDataSource ds = new JdbcDataSource();
+ ds.setURL("jdbc:h2:mem:" + dbName + ";DB_CLOSE_DELAY=-1");
+ DataSourceFactory.setInstance(ds);
+
+ manager = new DatabaseUserManager(mock(GroupManager.class));
+ manager.reload(); // initializes m_users from empty DB
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ try (Statement s = conn.createStatement()) {
+ s.execute("DROP ALL OBJECTS");
+ }
+ conn.close();
+ DataSourceFactory.close();
+ }
+
+ @Test
+ public void testSaveAndReloadUser() throws Exception {
+ final User user = buildUser("alice", "Alice Smith", "ROLE_USER", "ROLE_ADMIN");
+ addEmail(user, "alice@example.com");
+ user.addDutySchedule("MoTuWeThFr800-1700");
+
+ manager.saveUser("alice", user);
+
+ manager.reload();
+ final User loaded = manager.getUser("alice");
+
+ assertNotNull(loaded);
+ assertEquals("alice", loaded.getUserId());
+ assertEquals("Alice Smith", loaded.getFullName().orElse(null));
+ assertTrue(loaded.getRoles().contains("ROLE_USER"));
+ assertTrue(loaded.getRoles().contains("ROLE_ADMIN"));
+ assertEquals("alice@example.com", emailOf(loaded));
+ assertEquals(1, loaded.getDutySchedules().size());
+ assertEquals("MoTuWeThFr800-1700", loaded.getDutySchedules().get(0));
+ }
+
+ @Test
+ public void testDeleteUserCascadesToChildTables() throws Exception {
+ manager.saveUser("bob", buildUser("bob", "Bob Jones", "ROLE_USER"));
+ manager.reload();
+ assertNotNull(manager.getUser("bob"));
+
+ manager.deleteUser("bob");
+ manager.reload();
+ assertNull(manager.getUser("bob"));
+
+ // All child rows must be gone
+ assertRowCount(0, "user_roles", "bob");
+ assertRowCount(0, "user_contacts", "bob");
+ assertRowCount(0, "user_duty_schedules", "bob");
+ }
+
+ @Test
+ public void testRenameUser() throws Exception {
+ manager.saveUser("old", buildUser("old", "Original", "ROLE_USER"));
+ manager.reload();
+
+ manager.renameUser("old", "new");
+ manager.reload();
+
+ assertNull(manager.getUser("old"));
+ assertNotNull(manager.getUser("new"));
+ assertEquals("Original", manager.getUser("new").getFullName().orElse(null));
+ }
+
+ @Test
+ public void testPasswordRoundTrip() throws Exception {
+ final User user = buildUser("carol", "Carol", "ROLE_USER");
+ manager.saveUser("carol", user);
+
+ final String rawPw = "s3cr3t";
+ manager.setUnencryptedPassword("carol", rawPw);
+
+ manager.reload();
+ assertTrue(manager.comparePasswords("carol", rawPw));
+ }
+
+ @Test
+ public void testSaveUsersReplacesAll() throws Exception {
+ manager.saveUser("u1", buildUser("u1", "User 1", "ROLE_USER"));
+ manager.saveUser("u2", buildUser("u2", "User 2", "ROLE_USER"));
+ manager.reload();
+ assertEquals(2, manager.getUsers().size());
+
+ // saveUsers() with only u3 should replace both u1 and u2
+ manager.saveUsers(Arrays.asList(buildUser("u3", "User 3", "ROLE_ADMIN")));
+ manager.reload();
+
+ final Map users = manager.getUsers();
+ assertEquals(1, users.size());
+ assertTrue(users.containsKey("u3"));
+ assertFalse(users.containsKey("u1"));
+ }
+
+ @Test
+ public void testDuplicateContactTypeDeduplication() throws Exception {
+ final User user = buildUser("dup", "Dup User", "ROLE_USER");
+ // Add two email contacts — only last one should persist
+ final Contact c1 = new Contact();
+ c1.setType("email");
+ c1.setInfo("first@example.com");
+ user.addContact(c1);
+ final Contact c2 = new Contact();
+ c2.setType("email");
+ c2.setInfo("second@example.com");
+ user.addContact(c2);
+
+ manager.saveUser("dup", user); // should not throw UNIQUE constraint violation
+ manager.reload();
+
+ final User loaded = manager.getUser("dup");
+ final long emailCount = loaded.getContacts().stream()
+ .filter(c -> "email".equals(c.getType()))
+ .count();
+ assertEquals(1, emailCount);
+ assertEquals("second@example.com", emailOf(loaded));
+ }
+
+ @Test
+ public void testMigrateFromXmlOnEmptyDatabase() throws Exception {
+ final File tmpHome = Files.createTempDirectory("opennms-test").toFile();
+ final File etc = new File(tmpHome, "etc");
+ etc.mkdirs();
+
+ try (FileWriter fw = new FileWriter(new File(etc, "users.xml"))) {
+ fw.write("\n"
+ + "\n"
+ + " \n"
+ + " \n"
+ + " migrated\n"
+ + " Migrated User\n"
+ + " abc123\n"
+ + " \n"
+ + " \n"
+ + "\n");
+ }
+
+ final String prev = System.getProperty("opennms.home");
+ System.setProperty("opennms.home", tmpHome.getAbsolutePath());
+ try {
+ // Reload with empty DB triggers XML migration
+ manager.reload();
+ assertNotNull(manager.getUser("migrated"));
+ assertEquals("Migrated User", manager.getUser("migrated").getFullName().orElse(null));
+ } finally {
+ if (prev != null) System.setProperty("opennms.home", prev);
+ else System.clearProperty("opennms.home");
+ }
+ }
+
+ @Test
+ public void testMultipleUsersWithContactsAndDutySchedules() throws Exception {
+ for (int i = 0; i < 5; i++) {
+ final User u = buildUser("user" + i, "User " + i, "ROLE_USER");
+ addEmail(u, "user" + i + "@example.com");
+ u.addDutySchedule("MoTuWeThFr800-1700");
+ manager.saveUser("user" + i, u);
+ }
+
+ manager.reload();
+ assertEquals(5, manager.getUsers().size());
+
+ for (int i = 0; i < 5; i++) {
+ final User loaded = manager.getUser("user" + i);
+ assertEquals("user" + i + "@example.com", emailOf(loaded));
+ assertEquals(1, loaded.getDutySchedules().size());
+ }
+ }
+
+ @Test
+ public void testIsUserOnDuty() throws Exception {
+ final User u = buildUser("oncall", "On Call", "ROLE_USER");
+ u.addDutySchedule("MoTuWeThFrSaSu0000-2359"); // always on duty
+ manager.saveUser("oncall", u);
+
+ manager.reload();
+ assertTrue(manager.isUserOnDuty("oncall", java.util.Calendar.getInstance()));
+ }
+
+ @Test
+ public void testGetFileSize() throws Exception {
+ assertEquals(0L, manager.getFileSize());
+ }
+
+ @Test
+ public void testIsUpdateNeededAlwaysFalse() {
+ assertFalse(manager.isUpdateNeeded());
+ }
+
+ // -------------------------------------------------------------------------
+ // Helpers
+ // -------------------------------------------------------------------------
+
+ private static User buildUser(final String id, final String fullName, final String... roles) {
+ final User user = new User();
+ user.setUserId(id);
+ user.setFullName(fullName);
+ final Password pass = new Password();
+ pass.setEncryptedPassword("hashed_" + id);
+ pass.setSalt(true);
+ user.setPassword(pass);
+ for (final String role : roles) {
+ user.addRole(role);
+ }
+ return user;
+ }
+
+ private static void addEmail(final User user, final String email) {
+ final Contact c = new Contact();
+ c.setType("email");
+ c.setInfo(email);
+ user.addContact(c);
+ }
+
+ private static String emailOf(final User user) {
+ return user.getContacts().stream()
+ .filter(c -> "email".equals(c.getType()))
+ .findFirst()
+ .flatMap(Contact::getInfo)
+ .orElse(null);
+ }
+
+ private void assertRowCount(final int expected, final String table, final String userId) throws Exception {
+ try (Statement s = conn.createStatement();
+ ResultSet rs = s.executeQuery(
+ "SELECT COUNT(*) FROM " + table + " WHERE user_id = '" + userId + "'")) {
+ rs.next();
+ assertEquals(expected, rs.getInt(1));
+ }
+ }
+
+ private static void createSchema(final Connection c) throws Exception {
+ final Statement s = c.createStatement();
+ s.execute(
+ "CREATE TABLE users ("
+ + " user_id VARCHAR(256) PRIMARY KEY NOT NULL,"
+ + " full_name VARCHAR(256),"
+ + " user_comments TEXT,"
+ + " password VARCHAR(512) NOT NULL,"
+ + " password_salt BOOLEAN NOT NULL DEFAULT TRUE,"
+ + " tui_pin VARCHAR(32),"
+ + " time_zone_id VARCHAR(64),"
+ + " created_at TIMESTAMP NOT NULL DEFAULT NOW(),"
+ + " updated_at TIMESTAMP NOT NULL DEFAULT NOW()"
+ + ")"
+ );
+ s.execute(
+ "CREATE TABLE user_roles ("
+ + " id INT AUTO_INCREMENT PRIMARY KEY NOT NULL,"
+ + " user_id VARCHAR(256) NOT NULL REFERENCES users(user_id) ON DELETE CASCADE,"
+ + " role VARCHAR(128) NOT NULL,"
+ + " UNIQUE (user_id, role)"
+ + ")"
+ );
+ s.execute(
+ "CREATE TABLE user_contacts ("
+ + " id INT AUTO_INCREMENT PRIMARY KEY NOT NULL,"
+ + " user_id VARCHAR(256) NOT NULL REFERENCES users(user_id) ON DELETE CASCADE,"
+ + " contact_type VARCHAR(64) NOT NULL,"
+ + " contact_info TEXT,"
+ + " service_provider TEXT,"
+ + " UNIQUE (user_id, contact_type)"
+ + ")"
+ );
+ s.execute(
+ "CREATE TABLE user_duty_schedules ("
+ + " id INT AUTO_INCREMENT PRIMARY KEY NOT NULL,"
+ + " user_id VARCHAR(256) NOT NULL REFERENCES users(user_id) ON DELETE CASCADE,"
+ + " schedule VARCHAR(128) NOT NULL"
+ + ")"
+ );
+ s.close();
+ }
+}
diff --git a/opennms-webapp-rest/src/main/java/org/opennms/web/rest/v1/FilesystemRestService.java b/opennms-webapp-rest/src/main/java/org/opennms/web/rest/v1/FilesystemRestService.java
index 903ca14396d3..777a84a5c06e 100644
--- a/opennms-webapp-rest/src/main/java/org/opennms/web/rest/v1/FilesystemRestService.java
+++ b/opennms-webapp-rest/src/main/java/org/opennms/web/rest/v1/FilesystemRestService.java
@@ -59,7 +59,6 @@
import org.apache.commons.io.IOUtils;
import org.apache.cxf.jaxrs.ext.multipart.Attachment;
import org.apache.cxf.jaxrs.ext.multipart.Multipart;
-import org.opennms.core.utils.ConfigFileConstants;
import org.opennms.web.api.Authentication;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -84,18 +83,7 @@ public class FilesystemRestService {
"groovy",
"bsh",
"dcb");
- private final java.nio.file.Path usersXml;
-
public FilesystemRestService() {
- try {
- this.usersXml = ConfigFileConstants.getFile(ConfigFileConstants.USERS_CONF_FILE_NAME).toPath();
- } catch (IOException e) {
- throw new RuntimeException(e);
- }
- }
-
- FilesystemRestService(final java.nio.file.Path usersXml) {
- this.usersXml = usersXml;
}
private final java.nio.file.Path etcFolder = Paths.get(System.getProperty("opennms.home"), "etc");
@@ -111,7 +99,6 @@ public List getFiles(@QueryParam("changedFilesOnly") boolean changedFile
try {
return Files.find(etcFolder, 4, (path, basicFileAttributes) -> isSupportedExtension(path), FileVisitOption.FOLLOW_LINKS)
- .filter(p -> !p.equals(usersXml) || securityContext.isUserInRole(Authentication.ROLE_ADMIN))
.map(p -> etcFolder.relativize(p).toString())
.filter(p -> !changedFilesOnly || !doesFileExistAndMatchContentsWithEtcPristine(p, securityContext))
.sorted()
@@ -165,7 +152,8 @@ public Response getFileContents(@QueryParam("f") String fileName, @Context Secur
if (!securityContext.isUserInRole(Authentication.ROLE_FILESYSTEM_EDITOR)) {
throw new ForbiddenException("FILESYSTEM EDITOR role is required for reading files.");
}
- return fileContents(ensureFileIsAllowed(fileName, securityContext));
+ final java.nio.file.Path targetPath = ensureFileIsAllowed(fileName, securityContext);
+ return fileContents(targetPath);
}
@POST
@@ -239,10 +227,6 @@ private java.nio.file.Path ensureFileIsAllowed(String fileName, SecurityContext
final java.nio.file.Path etcFolderNormalized = etcFolder.normalize();
final java.nio.file.Path fileNormalized = etcFolder.resolve(fileName).normalize();
- if (fileNormalized.equals(usersXml) && !securityContext.isUserInRole(Authentication.ROLE_ADMIN)) {
- throw new ForbiddenException("ADMIN role is required for accessing users.xml file contents.");
- }
-
if (!(fileNormalized.getNameCount() > etcFolderNormalized.getNameCount() && fileNormalized.startsWith(etcFolderNormalized))) {
throw new BadRequestException("Cannot access files outside of folder! Filename given: " + fileName);
}
diff --git a/opennms-webapp-rest/src/main/java/org/opennms/web/rest/v1/WhoamiRestService.java b/opennms-webapp-rest/src/main/java/org/opennms/web/rest/v1/WhoamiRestService.java
index ba3c555a4937..deda2520ec50 100644
--- a/opennms-webapp-rest/src/main/java/org/opennms/web/rest/v1/WhoamiRestService.java
+++ b/opennms-webapp-rest/src/main/java/org/opennms/web/rest/v1/WhoamiRestService.java
@@ -67,7 +67,7 @@ public Response whoami(@Context final SecurityContext securityContext) {
userInfo.put("roles", userRoles);
userInfo.put("internal", false);
- // Check if the user exists in users.xml, if so it is an internal
+ // Check if the user exists in the database; if so it is an internal
// user and email, full name, etc. can be populated
try {
final OnmsUser onmsUser = userManager.getOnmsUser(userName);
diff --git a/opennms-webapp-rest/src/test/java/org/opennms/web/rest/v1/FilesystemRestServiceTest.java b/opennms-webapp-rest/src/test/java/org/opennms/web/rest/v1/FilesystemRestServiceTest.java
index 6641c91cc0cd..1447fc64dee6 100644
--- a/opennms-webapp-rest/src/test/java/org/opennms/web/rest/v1/FilesystemRestServiceTest.java
+++ b/opennms-webapp-rest/src/test/java/org/opennms/web/rest/v1/FilesystemRestServiceTest.java
@@ -58,7 +58,7 @@ public void before() throws IOException {
@Test
public void testXmlValidation() throws IOException {
- final FilesystemRestService filesystemRestService = new FilesystemRestService(null);
+ final FilesystemRestService filesystemRestService = new FilesystemRestService();
final HttpServer httpServer = HttpServer.create(new InetSocketAddress(1337), 10);
httpServer.createContext("/", new TestHttpHandler());
httpServer.setExecutor(null);
diff --git a/opennms-webapp/src/main/java/org/opennms/web/admin/users/AddNewUserServlet.java b/opennms-webapp/src/main/java/org/opennms/web/admin/users/AddNewUserServlet.java
index edc707e7e553..c1ae9052959b 100644
--- a/opennms-webapp/src/main/java/org/opennms/web/admin/users/AddNewUserServlet.java
+++ b/opennms-webapp/src/main/java/org/opennms/web/admin/users/AddNewUserServlet.java
@@ -54,11 +54,6 @@ public class AddNewUserServlet extends HttpServlet {
/** {@inheritDoc} */
@Override
public void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
- try {
- UserFactory.init();
- } catch (Throwable e) {
- throw new ServletException("AddNewUserServlet: Error initialising user factory." + e);
- }
UserManager userFactory = UserFactory.getInstance();
String userID = request.getParameter("userID");
@@ -73,7 +68,7 @@ public void doPost(HttpServletRequest request, HttpServletResponse response) thr
try {
hasUser = userFactory.hasUser(userID);
} catch (Throwable e) {
- throw new ServletException("can't determine if user " + userID + " already exists in users.xml.", e);
+ throw new ServletException("can't determine if user " + userID + " already exists in the database.", e);
}
if (hasUser) {
diff --git a/opennms-webapp/src/main/java/org/opennms/web/admin/users/DeleteUserServlet.java b/opennms-webapp/src/main/java/org/opennms/web/admin/users/DeleteUserServlet.java
index 71dc62173208..f187ad59013a 100644
--- a/opennms-webapp/src/main/java/org/opennms/web/admin/users/DeleteUserServlet.java
+++ b/opennms-webapp/src/main/java/org/opennms/web/admin/users/DeleteUserServlet.java
@@ -52,9 +52,7 @@ public class DeleteUserServlet extends HttpServlet {
public void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
String userID = request.getParameter("userID");
- // now save to the xml file
try {
- UserFactory.init();
UserManager userFactory = UserFactory.getInstance();
userFactory.deleteUser(userID);
} catch (Throwable e) {
diff --git a/opennms-webapp/src/main/java/org/opennms/web/admin/users/ModifyUserServlet.java b/opennms-webapp/src/main/java/org/opennms/web/admin/users/ModifyUserServlet.java
index 7d0d1b38bc1b..356918905f49 100644
--- a/opennms-webapp/src/main/java/org/opennms/web/admin/users/ModifyUserServlet.java
+++ b/opennms-webapp/src/main/java/org/opennms/web/admin/users/ModifyUserServlet.java
@@ -57,7 +57,6 @@ public void doPost(HttpServletRequest request, HttpServletResponse response) thr
HttpSession userSession = request.getSession(true);
try {
- UserFactory.init();
UserManager userFactory = UserFactory.getInstance();
User user = userFactory.getUser(request.getParameter("userID"));
userSession.setAttribute("user.modifyUser.jsp", user);
diff --git a/opennms-webapp/src/main/java/org/opennms/web/admin/users/RenameUserServlet.java b/opennms-webapp/src/main/java/org/opennms/web/admin/users/RenameUserServlet.java
index 51774a53e2c1..65a507354133 100644
--- a/opennms-webapp/src/main/java/org/opennms/web/admin/users/RenameUserServlet.java
+++ b/opennms-webapp/src/main/java/org/opennms/web/admin/users/RenameUserServlet.java
@@ -57,7 +57,6 @@ public void doPost(HttpServletRequest request, HttpServletResponse response) thr
throw new ServletException("User ID must not contain any HTML markup.");
}
- // now save to the xml file
try {
UserManager userFactory = UserFactory.getInstance();
userFactory.renameUser(userID, newID);
diff --git a/opennms-webapp/src/main/java/org/opennms/web/admin/users/SaveUserServlet.java b/opennms-webapp/src/main/java/org/opennms/web/admin/users/SaveUserServlet.java
index 51b231172827..ec714690ec77 100644
--- a/opennms-webapp/src/main/java/org/opennms/web/admin/users/SaveUserServlet.java
+++ b/opennms-webapp/src/main/java/org/opennms/web/admin/users/SaveUserServlet.java
@@ -60,7 +60,6 @@ public void doPost(HttpServletRequest request, HttpServletResponse response) thr
throw new ServletException("Error: user " + newUser.getUserId() + " is read-only!");
}
- // now save to the XML file
try {
userFactory.saveUser(newUser.getUserId(), newUser);
} catch (Throwable e) {
diff --git a/opennms-webapp/src/main/java/org/opennms/web/admin/users/UpdateUserServlet.java b/opennms-webapp/src/main/java/org/opennms/web/admin/users/UpdateUserServlet.java
index d8064563262b..1abb9bc37400 100644
--- a/opennms-webapp/src/main/java/org/opennms/web/admin/users/UpdateUserServlet.java
+++ b/opennms-webapp/src/main/java/org/opennms/web/admin/users/UpdateUserServlet.java
@@ -62,12 +62,6 @@ public void doPost(HttpServletRequest request, HttpServletResponse response) thr
if (userSession != null) {
User newUser = (User) userSession.getAttribute("user.modifyUser.jsp");
- try {
- UserFactory.init();
- } catch (Throwable e) {
- throw new ServletException("UpdateUserServlet:init Error initialising UserFactory " + e);
- }
-
// get the rest of the user information from the form
newUser.setFullName(request.getParameter("fullName"));
newUser.setUserComments(request.getParameter("userComments"));
@@ -106,52 +100,15 @@ public void doPost(HttpServletRequest request, HttpServletResponse response) thr
newUser.clearContacts();
- Contact tmpContact = new Contact();
- tmpContact.setInfo(email);
- tmpContact.setType(ContactType.email.toString());
- newUser.addContact(tmpContact);
-
- tmpContact = new Contact();
- tmpContact.setInfo(pagerEmail);
- tmpContact.setType(ContactType.pagerEmail.toString());
- newUser.addContact(tmpContact);
-
- tmpContact = new Contact();
- tmpContact.setInfo(xmppAddress);
- tmpContact.setType(ContactType.xmppAddress.toString());
- newUser.addContact(tmpContact);
-
- tmpContact = new Contact();
- tmpContact.setInfo(microblog);
- tmpContact.setType(ContactType.microblog.toString());
- newUser.addContact(tmpContact);
-
- tmpContact = new Contact();
- tmpContact.setInfo(numericPin);
- tmpContact.setServiceProvider(numericPage);
- tmpContact.setType(ContactType.numericPage.toString());
- newUser.addContact(tmpContact);
-
- tmpContact = new Contact();
- tmpContact.setInfo(textPin);
- tmpContact.setServiceProvider(textPage);
- tmpContact.setType(ContactType.textPage.toString());
- newUser.addContact(tmpContact);
-
- tmpContact = new Contact();
- tmpContact.setInfo(workPhone);
- tmpContact.setType(ContactType.workPhone.toString());
- newUser.addContact(tmpContact);
-
- tmpContact = new Contact();
- tmpContact.setInfo(mobilePhone);
- tmpContact.setType(ContactType.mobilePhone.toString());
- newUser.addContact(tmpContact);
-
- tmpContact = new Contact();
- tmpContact.setInfo(homePhone);
- tmpContact.setType(ContactType.homePhone.toString());
- newUser.addContact(tmpContact);
+ addContactIfNotBlank(newUser, ContactType.email.toString(), null, email);
+ addContactIfNotBlank(newUser, ContactType.pagerEmail.toString(), null, pagerEmail);
+ addContactIfNotBlank(newUser, ContactType.xmppAddress.toString(), null, xmppAddress);
+ addContactIfNotBlank(newUser, ContactType.microblog.toString(), null, microblog);
+ addContactIfNotBlank(newUser, ContactType.numericPage.toString(), numericPage, numericPin);
+ addContactIfNotBlank(newUser, ContactType.textPage.toString(), textPage, textPin);
+ addContactIfNotBlank(newUser, ContactType.workPhone.toString(), null, workPhone);
+ addContactIfNotBlank(newUser, ContactType.mobilePhone.toString(), null, mobilePhone);
+ addContactIfNotBlank(newUser, ContactType.homePhone.toString(), null, homePhone);
// build the duty schedule data structure
List newSchedule = new ArrayList(7);
@@ -201,5 +158,16 @@ public void doPost(HttpServletRequest request, HttpServletResponse response) thr
private List getDutySchedulesForUser(User newUser) {
return newUser.getDutySchedules();
}
-
+
+ private void addContactIfNotBlank(User user, String type, String serviceProvider, String info) {
+ if ((info != null && !info.trim().isEmpty()) || (serviceProvider != null && !serviceProvider.trim().isEmpty())) {
+ Contact c = new Contact();
+ c.setType(type);
+ c.setInfo(info);
+ if (serviceProvider != null && !serviceProvider.trim().isEmpty()) {
+ c.setServiceProvider(serviceProvider);
+ }
+ user.addContact(c);
+ }
+ }
}
diff --git a/opennms-webapp/src/main/webapp/WEB-INF/applicationContext-spring-security.xml b/opennms-webapp/src/main/webapp/WEB-INF/applicationContext-spring-security.xml
index 3bc8906fbfeb..b9f023822c03 100644
--- a/opennms-webapp/src/main/webapp/WEB-INF/applicationContext-spring-security.xml
+++ b/opennms-webapp/src/main/webapp/WEB-INF/applicationContext-spring-security.xml
@@ -444,7 +444,6 @@
-