Skip to content

Commit ecf9007

Browse files
authored
Implement database-based multi user system for Web UI (#1539)
* add UserDetailsService impl using Jooq * improve impl such that it is in a working condition * refactor: make github action checks happy * force data type JSON in Jooq for web_user.authorities reason: our build matrix fails for mysql, but succeeds for mariadb. Jooq infers data type org.jooq.JSON for web_user.authorities for mysql. on the other hand, it is String for mariadb. example: https://github.com/steve-community/steve/actions/runs/10339451112 * tighten json logic * add check for validating that "authorities" is an array * store a sorted set of authorities without duplicates * add method to delete web user by database id reason: to be used by web pages. a better way than doing with username, and is consistent with other delete operations we do. * PR feedback: skip default admin user creation, if "any" admin already exists * refactor: PR feedback * prepare database for #1540 * PR feedback * add license header where missing
1 parent 8a2376c commit ecf9007

File tree

6 files changed

+395
-16
lines changed

6 files changed

+395
-16
lines changed

pom.xml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -434,6 +434,10 @@
434434
<name>BOOLEAN</name>
435435
<includeExpression>.*\.OCPP_TAG_ACTIVITY\.(IN_TRANSACTION|BLOCKED)</includeExpression>
436436
</forcedType>
437+
<forcedType>
438+
<name>JSON</name>
439+
<includeExpression>.*\.WEB_USER\.(AUTHORITIES)</includeExpression>
440+
</forcedType>
437441
<forcedType>
438442
<userType>org.joda.time.DateTime</userType>
439443
<converter>de.rwth.idsg.steve.utils.DateTimeConverter</converter>

src/main/java/de/rwth/idsg/steve/config/SecurityConfiguration.java

Lines changed: 3 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import com.fasterxml.jackson.databind.ObjectMapper;
2222
import com.google.common.base.Strings;
2323
import de.rwth.idsg.steve.web.api.ApiControllerAdvice;
24+
import lombok.RequiredArgsConstructor;
2425
import lombok.extern.slf4j.Slf4j;
2526
import org.springframework.context.annotation.Bean;
2627
import org.springframework.context.annotation.Configuration;
@@ -34,13 +35,9 @@
3435
import org.springframework.security.config.http.SessionCreationPolicy;
3536
import org.springframework.security.core.Authentication;
3637
import org.springframework.security.core.AuthenticationException;
37-
import org.springframework.security.core.userdetails.User;
38-
import org.springframework.security.core.userdetails.UserDetails;
39-
import org.springframework.security.core.userdetails.UserDetailsService;
4038
import org.springframework.security.crypto.factory.PasswordEncoderFactories;
4139
import org.springframework.security.crypto.password.DelegatingPasswordEncoder;
4240
import org.springframework.security.crypto.password.PasswordEncoder;
43-
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
4441
import org.springframework.security.web.AuthenticationEntryPoint;
4542
import org.springframework.security.web.SecurityFilterChain;
4643
import org.springframework.security.web.authentication.preauth.AbstractPreAuthenticatedProcessingFilter;
@@ -58,6 +55,7 @@
5855
* @since 07.01.2015
5956
*/
6057
@Slf4j
58+
@RequiredArgsConstructor
6159
@Configuration
6260
@EnableWebSecurity
6361
public class SecurityConfiguration {
@@ -74,17 +72,6 @@ public PasswordEncoder passwordEncoder() {
7472
return CONFIG.getAuth().getPasswordEncoder();
7573
}
7674

77-
@Bean
78-
public UserDetailsService userDetailsService() {
79-
UserDetails webPageUser = User.builder()
80-
.username(CONFIG.getAuth().getUserName())
81-
.password(CONFIG.getAuth().getEncodedPassword())
82-
.roles("ADMIN")
83-
.build();
84-
85-
return new InMemoryUserDetailsManager(webPageUser);
86-
}
87-
8875
@Bean
8976
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
9077
final String prefix = CONFIG.getSpringManagerMapping();
@@ -98,7 +85,7 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti
9885
WebSocketConfiguration.PATH_INFIX + "**",
9986
"/WEB-INF/views/**" // https://github.com/spring-projects/spring-security/issues/13285#issuecomment-1579097065
10087
).permitAll()
101-
.requestMatchers(prefix + "/**").hasRole("ADMIN")
88+
.requestMatchers(prefix + "/**").hasAuthority("ADMIN")
10289
)
10390
// SOAP stations are making POST calls for communication. even though the following path is permitted for
10491
// all access, there is a global default behaviour from spring security: enable CSRF for all POSTs.
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
/*
2+
* SteVe - SteckdosenVerwaltung - https://github.com/steve-community/steve
3+
* Copyright (C) 2013-2024 SteVe Community Team
4+
* All Rights Reserved.
5+
*
6+
* This program is free software: you can redistribute it and/or modify
7+
* it under the terms of the GNU General Public License as published by
8+
* the Free Software Foundation, either version 3 of the License, or
9+
* (at your option) any later version.
10+
*
11+
* This program is distributed in the hope that it will be useful,
12+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
13+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14+
* GNU General Public License for more details.
15+
*
16+
* You should have received a copy of the GNU General Public License
17+
* along with this program. If not, see <https://www.gnu.org/licenses/>.
18+
*/
19+
package de.rwth.idsg.steve.repository;
20+
21+
import jooq.steve.db.tables.records.WebUserRecord;
22+
23+
public interface WebUserRepository {
24+
25+
void createUser(WebUserRecord user);
26+
27+
void updateUser(WebUserRecord user);
28+
29+
void deleteUser(String username);
30+
31+
void deleteUser(int webUserPk);
32+
33+
void changeStatusOfUser(String username, boolean enabled);
34+
35+
Integer getUserCountWithAuthority(String authority);
36+
37+
void changePassword(String username, String newPassword);
38+
39+
boolean userExists(String username);
40+
41+
WebUserRecord loadUserByUsername(String username);
42+
}
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
/*
2+
* SteVe - SteckdosenVerwaltung - https://github.com/steve-community/steve
3+
* Copyright (C) 2013-2024 SteVe Community Team
4+
* All Rights Reserved.
5+
*
6+
* This program is free software: you can redistribute it and/or modify
7+
* it under the terms of the GNU General Public License as published by
8+
* the Free Software Foundation, either version 3 of the License, or
9+
* (at your option) any later version.
10+
*
11+
* This program is distributed in the hope that it will be useful,
12+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
13+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14+
* GNU General Public License for more details.
15+
*
16+
* You should have received a copy of the GNU General Public License
17+
* along with this program. If not, see <https://www.gnu.org/licenses/>.
18+
*/
19+
package de.rwth.idsg.steve.repository.impl;
20+
21+
import de.rwth.idsg.steve.repository.WebUserRepository;
22+
import jooq.steve.db.tables.records.WebUserRecord;
23+
import lombok.RequiredArgsConstructor;
24+
import lombok.extern.slf4j.Slf4j;
25+
import org.jooq.DSLContext;
26+
import org.jooq.JSON;
27+
import org.springframework.stereotype.Repository;
28+
29+
import static jooq.steve.db.Tables.WEB_USER;
30+
import static org.jooq.impl.DSL.condition;
31+
import static org.jooq.impl.DSL.count;
32+
33+
/**
34+
* @author Sevket Goekay <[email protected]>
35+
* @since 10.08.2024
36+
*/
37+
@Slf4j
38+
@Repository
39+
@RequiredArgsConstructor
40+
public class WebUserRepositoryImpl implements WebUserRepository {
41+
42+
private final DSLContext ctx;
43+
44+
@Override
45+
public void createUser(WebUserRecord user) {
46+
ctx.insertInto(WEB_USER)
47+
.set(WEB_USER.USERNAME, user.getUsername())
48+
.set(WEB_USER.PASSWORD, user.getPassword())
49+
.set(WEB_USER.ENABLED, user.getEnabled())
50+
.set(WEB_USER.AUTHORITIES, user.getAuthorities())
51+
.execute();
52+
}
53+
54+
@Override
55+
public void updateUser(WebUserRecord user) {
56+
ctx.update(WEB_USER)
57+
.set(WEB_USER.PASSWORD, user.getPassword())
58+
.set(WEB_USER.ENABLED, user.getEnabled())
59+
.set(WEB_USER.AUTHORITIES, user.getAuthorities())
60+
.where(WEB_USER.USERNAME.eq(user.getUsername()))
61+
.execute();
62+
}
63+
64+
@Override
65+
public void deleteUser(String username) {
66+
ctx.delete(WEB_USER)
67+
.where(WEB_USER.USERNAME.eq(username))
68+
.execute();
69+
}
70+
71+
@Override
72+
public void deleteUser(int webUserPk) {
73+
ctx.delete(WEB_USER)
74+
.where(WEB_USER.WEB_USER_PK.eq(webUserPk))
75+
.execute();
76+
}
77+
78+
@Override
79+
public void changeStatusOfUser(String username, boolean enabled) {
80+
ctx.update(WEB_USER)
81+
.set(WEB_USER.ENABLED, enabled)
82+
.where(WEB_USER.USERNAME.eq(username))
83+
.execute();
84+
}
85+
86+
@Override
87+
public Integer getUserCountWithAuthority(String authority) {
88+
JSON authValue = JSON.json("\"" + authority + "\"");
89+
return ctx.selectCount()
90+
.from(WEB_USER)
91+
.where(condition("json_contains({0}, {1})", WEB_USER.AUTHORITIES, authValue))
92+
.fetchOne(count());
93+
}
94+
95+
@Override
96+
public void changePassword(String username, String newPassword) {
97+
ctx.update(WEB_USER)
98+
.set(WEB_USER.PASSWORD, newPassword)
99+
.where(WEB_USER.USERNAME.eq(username))
100+
.execute();
101+
}
102+
103+
@Override
104+
public boolean userExists(String username) {
105+
return ctx.selectOne()
106+
.from(WEB_USER)
107+
.where(WEB_USER.USERNAME.eq(username))
108+
.fetchOptional()
109+
.isPresent();
110+
}
111+
112+
@Override
113+
public WebUserRecord loadUserByUsername(String username) {
114+
return ctx.selectFrom(WEB_USER)
115+
.where(WEB_USER.USERNAME.eq(username))
116+
.fetchOne();
117+
}
118+
}

0 commit comments

Comments
 (0)