Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Change from single user (config) to database based multi user system. #1165

Open
wants to merge 31 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
ae0d9d2
Change from single user (config) to database based multi user system.
Jun 2, 2023
61e3f00
style checks
Jun 2, 2023
7a15857
delete noAccessController to rename
Jun 2, 2023
9d84a5a
add renamed NoAccessController
Jun 2, 2023
1359cc8
style check
Jun 2, 2023
cc3db61
Removed out commented code; cleaned the import areas, removed useless…
Jun 3, 2023
4f51fc3
Increased spring-boot version to 3.1.0
Jun 3, 2023
45d9ab3
Revert Commit, because not Java 11 compatible
Jun 3, 2023
7aa3bd6
adding missing newline at the file end (style check)
Jun 3, 2023
6564cdc
WebUserController: correct return url if passwort is blank
Jun 5, 2023
edd2365
BeanConfiguration: added @Autowired to HikariDataSource and @Bean to …
Jun 5, 2023
778318f
SecurityConfiguration: changed DataSource to HikariDataSource and add…
Jun 5, 2023
fa7a2fc
pom.xml: revomed dependency of org.springframework.boot and moved dep…
Jun 5, 2023
54e7198
BeanConfiguration: initDataSource returns the new HikariDataSourcedir…
Jun 5, 2023
98b9ce8
BeanConfiguration: renamed initDataSource() --> dataSource(); removed…
Jun 6, 2023
040878c
NoAccessController: removed the RequestMethod, so all requests can be…
Jun 6, 2023
647ca1c
SecurityConfiguration: adapt SecurityFilterChain, so Usesr have "read"
Jun 6, 2023
42980df
Merge branch 'steve-community:master' into MultiUsers
fnkbsi Nov 28, 2023
0236b55
Merge branch 'steve-community:master' into MultiUsers
fnkbsi Dec 13, 2023
0276925
Rename db migration script to avoid conflict with master. Included if…
Dec 24, 2023
bd62f19
Rename db migration script removed "if not exist" clauses because of …
Dec 24, 2023
2f67258
Merge db migration script V1_0_4 from master
Dec 24, 2023
6fde7dc
Merge branch 'steve-community:master' into MultiUsers
fnkbsi Dec 24, 2023
d2273f7
Merge branch 'steve-community:master' into MultiUsers
fnkbsi Jan 15, 2024
4a7a616
Merge branch 'steve-community:master' into MultiUsers
fnkbsi Feb 1, 2024
ab84315
Merge branch 'steve-community:master' into MultiUsers
fnkbsi Feb 6, 2024
ed4e602
Merge branch 'steve-community:master' into MultiUsers
fnkbsi Feb 19, 2024
6a6c96a
Merge branch 'steve-community:master' into MultiUsers
fnkbsi Mar 28, 2024
a3d6d16
Merge branch 'steve-community:master' into MultiUsers
fnkbsi Apr 3, 2024
a3f2fd2
Merge branch 'steve-community:master' into MultiUsers
fnkbsi Apr 19, 2024
291f683
Merge branch 'steve-community:master' into MultiUsers
fnkbsi May 13, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -607,6 +607,11 @@
<groupId>org.springframework</groupId>
<artifactId>spring-websocket</artifactId>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
<type>jar</type>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-web</artifactId>
Expand Down
12 changes: 8 additions & 4 deletions src/main/java/de/rwth/idsg/steve/config/BeanConfiguration.java
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
import com.zaxxer.hikari.HikariConfig;
import com.zaxxer.hikari.HikariDataSource;
import de.rwth.idsg.steve.SteveConfiguration;
import static de.rwth.idsg.steve.SteveConfiguration.CONFIG;
import de.rwth.idsg.steve.service.DummyReleaseCheckService;
import de.rwth.idsg.steve.service.GithubReleaseCheckService;
import de.rwth.idsg.steve.service.ReleaseCheckService;
Expand Down Expand Up @@ -67,7 +68,7 @@
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.TimeUnit;

import static de.rwth.idsg.steve.SteveConfiguration.CONFIG;


/**
* Configuration and beans of Spring Framework.
Expand All @@ -88,7 +89,8 @@ public class BeanConfiguration implements WebMvcConfigurer {
/**
* https://github.com/brettwooldridge/HikariCP/wiki/MySQL-Configuration
*/
private void initDataSource() {
@Bean
public HikariDataSource dataSource() {
SteveConfiguration.DB dbConfig = CONFIG.getDb();

HikariConfig hc = new HikariConfig();
Expand All @@ -110,7 +112,7 @@ private void initDataSource() {
// https://github.com/steve-community/steve/issues/736
hc.setMaxLifetime(580_000);

dataSource = new HikariDataSource(hc);
return new HikariDataSource(hc);
}

/**
Expand All @@ -127,7 +129,9 @@ private void initDataSource() {
*/
@Bean
public DSLContext dslContext() {
initDataSource();
Copy link
Contributor

Choose a reason for hiding this comment

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

HikariDataSource dataSource = initDataSource(); // better if named dataSource()

if (dataSource == null) {
dataSource = dataSource();
}

Settings settings = new Settings()
// Normally, the records are "attached" to the Configuration that created (i.e. fetch/insert) them.
Expand Down
139 changes: 110 additions & 29 deletions src/main/java/de/rwth/idsg/steve/config/SecurityConfiguration.java
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,10 @@
*/
package de.rwth.idsg.steve.config;

import static de.rwth.idsg.steve.SteveConfiguration.CONFIG;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.datatype.joda.JodaModule;
import com.google.common.base.Strings;
import com.zaxxer.hikari.HikariDataSource;
import de.rwth.idsg.steve.SteveProdCondition;
import de.rwth.idsg.steve.web.api.ApiControllerAdvice;
import lombok.extern.slf4j.Slf4j;
Expand All @@ -38,13 +39,7 @@
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.factory.PasswordEncoderFactories;
import org.springframework.security.crypto.password.DelegatingPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.preauth.AbstractPreAuthenticatedProcessingFilter;
Expand All @@ -54,10 +49,14 @@
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

import static de.rwth.idsg.steve.SteveConfiguration.CONFIG;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.provisioning.JdbcUserDetailsManager;
import org.springframework.security.provisioning.UserDetailsManager;

/**
* @author Sevket Goekay <[email protected]>
* @author Frank Brosi
* @since 07.01.2015
*/
@Slf4j
Expand All @@ -66,29 +65,27 @@
@Conditional(SteveProdCondition.class)
public class SecurityConfiguration {

@Autowired
private HikariDataSource dataSource;


Check failure on line 71 in src/main/java/de/rwth/idsg/steve/config/SecurityConfiguration.java

View workflow job for this annotation

GitHub Actions / checkstyle

[checkstyle] reported by reviewdog 🐶 Line has trailing spaces. Raw Output: /github/workspace/./src/main/java/de/rwth/idsg/steve/config/SecurityConfiguration.java:71:0: error: Line has trailing spaces. (com.puppycrawl.tools.checkstyle.checks.regexp.RegexpSinglelineCheck)
/**
* Password encoding changed with spring-security 5.0.0. We either have to use a prefix before the password to
* indicate which actual encoder {@link DelegatingPasswordEncoder} should use [1, 2] or specify the encoder as we do.
*
* [1] https://spring.io/blog/2017/11/01/spring-security-5-0-0-rc1-released#password-storage-format
* [2] {@link PasswordEncoderFactories#createDelegatingPasswordEncoder()}
*/

/**
Copy link
Contributor

Choose a reason for hiding this comment

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

Useless change

*
* @return
*/
@Bean
public PasswordEncoder passwordEncoder() {
return CONFIG.getAuth().getPasswordEncoder();
}

@Bean
public UserDetailsService userDetailsService() {
UserDetails webPageUser = User.builder()
.username(CONFIG.getAuth().getUserName())
.password(CONFIG.getAuth().getEncodedPassword())
.roles("ADMIN")
.build();

return new InMemoryUserDetailsManager(webPageUser);
}

@Bean
public WebSecurityCustomizer webSecurityCustomizer() {
return (web) -> web.ignoring().antMatchers(
Expand All @@ -100,23 +97,107 @@
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
final String prefix = CONFIG.getSpringManagerMapping();

return http
.authorizeHttpRequests(
req -> req.antMatchers(prefix + "/**").hasRole("ADMIN")
)
.authorizeRequests(
Copy link
Contributor

Choose a reason for hiding this comment

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

What are the expected rights?

On my side, I'd love that users can read but not create/update/delete.
Admin can do everything.

req -> req
.antMatchers(prefix + "/home").hasAnyRole("USER", "ADMIN")
// webuser
.antMatchers(prefix + "/webusers").hasAnyRole("USER", "ADMIN")
.antMatchers(prefix + "/webusers" + "/details/**").hasAnyRole("USER", "ADMIN")
// users
.antMatchers(prefix + "/users").hasAnyRole("USER", "ADMIN")
.antMatchers(prefix + "/users" + "/details/**").hasAnyRole("USER", "ADMIN")
//ocppTags
.antMatchers(prefix + "/ocppTags").hasAnyRole("USER", "ADMIN")
.antMatchers(prefix + "/ocppTags" + "/details/**").hasAnyRole("USER", "ADMIN")
// chargepoints
.antMatchers(prefix + "/chargepoints").hasAnyRole("USER", "ADMIN")
.antMatchers(prefix + "/chargepoints" + "/details/**").hasAnyRole("USER", "ADMIN")
// transactions and reservations
.antMatchers(prefix + "/transactions").hasAnyRole("USER", "ADMIN")
.antMatchers(prefix + "/transactions" + "/details/**").hasAnyRole("USER", "ADMIN")
.antMatchers(prefix + "/reservations").hasAnyRole("USER", "ADMIN")
.antMatchers(prefix + "/reservations" + "/**").hasRole("ADMIN")
// singout and noAccess
.antMatchers(prefix + "/signout/" + "**").hasAnyRole("USER", "ADMIN")
.antMatchers(prefix + "/noAccess/" + "**").hasAnyRole("USER", "ADMIN")
// any other site
.antMatchers(prefix + "/**").hasRole("ADMIN")
Copy link
Contributor

Choose a reason for hiding this comment

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

Don't forget to manage the api route

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The api route is managed by the api key security filter chain.
A similar approach for the api key or a combination of api key and user/password is possible, but needs to be discussed.

Copy link
Contributor

Choose a reason for hiding this comment

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

Right. I forgot it 😅

)
.sessionManagement(
req -> req.invalidSessionUrl(prefix + "/signin")
)
req -> req
.invalidSessionUrl(prefix + "/signin")
)
.formLogin(
req -> req.loginPage(prefix + "/signin").permitAll()
)
req -> req
.loginPage(prefix + "/signin")
.permitAll()
)
.logout(
req -> req.logoutUrl(prefix + "/signout")
)
req -> req
.logoutUrl(prefix + "/signout")
)

.exceptionHandling(
req -> req
.accessDeniedPage(prefix + "/noAccess")
)
.build();
}

/**
*
* @param auth
* @throws Exception
*/
@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth)
throws Exception {
auth.jdbcAuthentication()
.passwordEncoder(CONFIG.getAuth().getPasswordEncoder())
.dataSource(dataSource)
.usersByUsernameQuery("select username,password,enabled from webusers where username = ?")
.authoritiesByUsernameQuery("select username,authority from webauthorities where username = ?");
}


/**
*
* @return
*/
@Bean
public UserDetailsManager authenticateUsers() {
JdbcUserDetailsManager users = new JdbcUserDetailsManager(dataSource);
Copy link
Contributor

Choose a reason for hiding this comment

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

To be discussed but I think a home-made JooqUserDetailsManager is not a difficult task and will avoid an extra dependency.

// Adapt the SQL-Commands to the correct table names (user -> webuser; authorities -> webauthorities)
users.setAuthoritiesByUsernameQuery("select username,authority from webauthorities where username=?");
users.setUsersByUsernameQuery("select username,password,enabled from webusers where username=?");

/* Adding the admin from config-file to the database/webusers
--> changing the name in the file will not delete the corresponding webuser in the database!
users.setCreateUserSql("insert into webusers (username, password, enabled) values (?,?,?)");
users.setCreateAuthoritySql("insert into webauthorities (username, authority) values (?,?)");
users.setUserExistsSql("select username from webusers where username = ?");
users.setUpdateUserSql("update webusers set password = ?, enabled = ? where username = ?");
users.setDeleteUserAuthoritiesSql("delete from webauthorities where username = ?");

UserDetails localUser = User.builder()
.username(CONFIG.getAuth().getUserName())
.password(CONFIG.getAuth().getEncodedPassword())
.roles("USER")
.build();

if (users.userExists(CONFIG.getAuth().getUserName()))
{
users.updateUser(localUser);
}
else
{
users.createUser(localUser);
}
*/
return users;
}

@Bean
@Order(1)
public SecurityFilterChain apiKeyFilterChain(HttpSecurity http, ObjectMapper objectMapper) throws Exception {
Expand Down
39 changes: 39 additions & 0 deletions src/main/java/de/rwth/idsg/steve/repository/WebUserRepository.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/*
* SteVe - SteckdosenVerwaltung - https://github.com/steve-community/steve
* Copyright (C) 2013-2023 SteVe Community Team
* All Rights Reserved.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package de.rwth.idsg.steve.repository;

import de.rwth.idsg.steve.repository.dto.WebUser;
import de.rwth.idsg.steve.web.dto.WebUserForm;
import de.rwth.idsg.steve.web.dto.WebUserQueryForm;

import java.util.List;

/**
* @author Frank Brosi
* @since 21.03.2022
*/
public interface WebUserRepository {
List<WebUser.Overview> getOverview(WebUserQueryForm form);
WebUser.Details getDetails(String webusername);

void add(WebUserForm form);
void update(WebUserForm form);
void delete(String webusername, String role);
void delete(String webusername);
}
46 changes: 46 additions & 0 deletions src/main/java/de/rwth/idsg/steve/repository/dto/WebUser.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
/*
* SteVe - SteckdosenVerwaltung - https://github.com/steve-community/steve
* Copyright (C) 2013-2023 SteVe Community Team
* All Rights Reserved.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package de.rwth.idsg.steve.repository.dto;

import java.util.List;
import jooq.steve.db.tables.records.WebauthoritiesRecord;
import jooq.steve.db.tables.records.WebusersRecord;
import lombok.Builder;
import lombok.Getter;

/**
* @author Frank Brosi
* @since 01.04.2022
*/
public class WebUser {

@Getter
@Builder
public static final class Overview {
private final Boolean enabled;
private final String webusername, roles;
}

@Getter
@Builder
public static final class Details {
private final WebusersRecord webusersRecord;
private final List<WebauthoritiesRecord> webauthoritiesRecordList;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,129 +16,137 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package de.rwth.idsg.steve.repository.impl;

import de.rwth.idsg.steve.repository.GenericRepository;
import de.rwth.idsg.steve.repository.ReservationStatus;
import de.rwth.idsg.steve.repository.dto.DbVersion;
import de.rwth.idsg.steve.utils.DateTimeUtils;
import de.rwth.idsg.steve.web.dto.Statistics;
import lombok.extern.slf4j.Slf4j;
import org.joda.time.DateTime;
import org.jooq.DSLContext;
import org.jooq.Field;
import org.jooq.Record2;
import org.jooq.Record8;
import org.jooq.Record9;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Repository;

import static de.rwth.idsg.steve.utils.CustomDSL.date;
import static jooq.steve.db.Tables.RESERVATION;
import static jooq.steve.db.Tables.TRANSACTION;
import static jooq.steve.db.tables.ChargeBox.CHARGE_BOX;
import static jooq.steve.db.tables.OcppTag.OCPP_TAG;
import static jooq.steve.db.tables.SchemaVersion.SCHEMA_VERSION;
import static jooq.steve.db.tables.User.USER;
import static jooq.steve.db.tables.Webusers.WEBUSERS;
import static org.jooq.impl.DSL.max;
import static org.jooq.impl.DSL.select;

/**
* @author Sevket Goekay <[email protected]>
* @since 14.08.2014
*/
@Slf4j
@Repository
public class GenericRepositoryImpl implements GenericRepository {

@Autowired private DSLContext ctx;

@Override
public Statistics getStats() {
DateTime now = DateTime.now();
DateTime yesterdaysNow = now.minusDays(1);

Field<Integer> numChargeBoxes =
ctx.selectCount()
.from(CHARGE_BOX)
.asField("num_charge_boxes");

Field<Integer> numOcppTags =
ctx.selectCount()
.from(OCPP_TAG)
.asField("num_ocpp_tags");

Field<Integer> numUsers =
ctx.selectCount()
.from(USER)
.asField("num_users");

Field<Integer> numReservations =
ctx.selectCount()
.from(RESERVATION)
.where(RESERVATION.EXPIRY_DATETIME.greaterThan(now))
.and(RESERVATION.STATUS.eq(ReservationStatus.ACCEPTED.name()))
.asField("num_reservations");

Field<Integer> numTransactions =
ctx.selectCount()
.from(TRANSACTION)
.where(TRANSACTION.STOP_TIMESTAMP.isNull())
.asField("num_transactions");

Field<Integer> heartbeatsToday =
ctx.selectCount()
.from(CHARGE_BOX)
.where(date(CHARGE_BOX.LAST_HEARTBEAT_TIMESTAMP).eq(date(now)))
.asField("heartbeats_today");

Field<Integer> heartbeatsYesterday =
ctx.selectCount()
.from(CHARGE_BOX)
.where(date(CHARGE_BOX.LAST_HEARTBEAT_TIMESTAMP).eq(date(yesterdaysNow)))
.asField("heartbeats_yesterday");

Field<Integer> heartbeatsEarlier =
ctx.selectCount()
.from(CHARGE_BOX)
.where(date(CHARGE_BOX.LAST_HEARTBEAT_TIMESTAMP).lessThan(date(yesterdaysNow)))
.asField("heartbeats_earlier");

Record8<Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer> gs =
Field<Integer> numWebUsers =
ctx.selectCount()
.from(WEBUSERS)
.asField("num_webusers");

Record9<Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer> gs =
ctx.select(
numChargeBoxes,
numOcppTags,
numUsers,
numReservations,
numTransactions,
heartbeatsToday,
heartbeatsYesterday,
heartbeatsEarlier
heartbeatsEarlier,
numWebUsers
).fetchOne();

return Statistics.builder()
.numChargeBoxes(gs.value1())
.numOcppTags(gs.value2())
.numUsers(gs.value3())
.numReservations(gs.value4())
.numTransactions(gs.value5())
.heartbeatToday(gs.value6())
.heartbeatYesterday(gs.value7())
.heartbeatEarlier(gs.value8())
.numWebUsers(gs.value9())
.build();
}

@Override
public DbVersion getDBVersion() {
Record2<String, DateTime> record = ctx.select(SCHEMA_VERSION.VERSION, SCHEMA_VERSION.INSTALLED_ON)
.from(SCHEMA_VERSION)
.where(SCHEMA_VERSION.INSTALLED_RANK.eq(
select(max(SCHEMA_VERSION.INSTALLED_RANK)).from(SCHEMA_VERSION)))
.fetchOne();

String ts = DateTimeUtils.humanize(record.value2());
return DbVersion.builder()
.version(record.value1())
.updateTimestamp(ts)
.build();
}
}

Check warning on line 152 in src/main/java/de/rwth/idsg/steve/repository/impl/GenericRepositoryImpl.java

View workflow job for this annotation

GitHub Actions / pmd

Too many static imports may lead to messy code

If you overuse the static import feature, it can make your program unreadable and unmaintainable, polluting its namespace with all the static members you import. Readers of your code (including you, a few months after you wrote it) will not know which class a static member comes from (Sun 1.5 Language Guide). TooManyStaticImports (Priority: 3, Ruleset: Code Style) https://docs.pmd-code.org/pmd-doc-7.1.0/pmd_rules_java_codestyle.html#toomanystaticimports
Loading