Skip to content

Commit cb5af53

Browse files
committed
Added website stuff for Web-User administarion and additional requestMatchers
1 parent ecf9007 commit cb5af53

File tree

18 files changed

+880
-12
lines changed

18 files changed

+880
-12
lines changed

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

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,27 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti
8686
"/WEB-INF/views/**" // https://github.com/spring-projects/spring-security/issues/13285#issuecomment-1579097065
8787
).permitAll()
8888
.requestMatchers(prefix + "/**").hasAuthority("ADMIN")
89+
.requestMatchers(prefix + "/home").hasAnyAuthority("USER", "ADMIN")
90+
// webuser
91+
.requestMatchers(prefix + "/webusers").hasAnyAuthority("USER", "ADMIN")
92+
.requestMatchers(prefix + "/webusers" + "/details/**").hasAnyAuthority("USER", "ADMIN")
93+
// users
94+
.requestMatchers(prefix + "/users").hasAnyAuthority("USER", "ADMIN")
95+
.requestMatchers(prefix + "/users" + "/details/**").hasAnyAuthority("USER", "ADMIN")
96+
//ocppTags
97+
.requestMatchers(prefix + "/ocppTags").hasAnyAuthority("USER", "ADMIN")
98+
.requestMatchers(prefix + "/ocppTags" + "/details/**").hasAnyAuthority("USER", "ADMIN")
99+
// chargepoints
100+
.requestMatchers(prefix + "/chargepoints").hasAnyAuthority("USER", "ADMIN")
101+
.requestMatchers(prefix + "/chargepoints" + "/details/**").hasAnyAuthority("USER", "ADMIN")
102+
// transactions and reservations
103+
.requestMatchers(prefix + "/transactions").hasAnyAuthority("USER", "ADMIN")
104+
.requestMatchers(prefix + "/transactions" + "/details/**").hasAnyAuthority("USER", "ADMIN")
105+
.requestMatchers(prefix + "/reservations").hasAnyAuthority("USER", "ADMIN")
106+
.requestMatchers(prefix + "/reservations" + "/**").hasAnyAuthority("ADMIN")
107+
// singout and noAccess
108+
.requestMatchers(prefix + "/signout/" + "**").hasAnyAuthority("USER", "ADMIN")
109+
.requestMatchers(prefix + "/noAccess/" + "**").hasAnyAuthority("USER", "ADMIN")
89110
)
90111
// SOAP stations are making POST calls for communication. even though the following path is permitted for
91112
// all access, there is a global default behaviour from spring security: enable CSRF for all POSTs.
@@ -100,6 +121,9 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti
100121
.logout(
101122
req -> req.logoutUrl(prefix + "/signout")
102123
)
124+
.exceptionHandling(
125+
req -> req.accessDeniedPage(prefix + "/noAccess")
126+
)
103127
.build();
104128
}
105129

src/main/java/de/rwth/idsg/steve/repository/WebUserRepository.java

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,9 @@
1919
package de.rwth.idsg.steve.repository;
2020

2121
import jooq.steve.db.tables.records.WebUserRecord;
22+
import de.rwth.idsg.steve.repository.dto.WebUserOverview;
23+
import de.rwth.idsg.steve.web.dto.WebUserQueryForm;
24+
import java.util.List;
2225

2326
public interface WebUserRepository {
2427

@@ -38,5 +41,9 @@ public interface WebUserRepository {
3841

3942
boolean userExists(String username);
4043

44+
WebUserRecord loadUserByUsePk(Integer webUserPk);
4145
WebUserRecord loadUserByUsername(String username);
42-
}
46+
47+
List<WebUserOverview> getOverview(WebUserQueryForm form);
48+
49+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
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.dto;
20+
import lombok.Builder;
21+
import lombok.Getter;
22+
23+
@Getter
24+
@Builder
25+
public final class WebUserOverview {
26+
27+
private final Integer webUserPk;
28+
private final Boolean enabled;
29+
private final String webusername;
30+
private final String[] autorithies;
31+
}

src/main/java/de/rwth/idsg/steve/repository/impl/GenericRepositoryImpl.java

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@
2828
import org.jooq.DSLContext;
2929
import org.jooq.Field;
3030
import org.jooq.Record2;
31-
import org.jooq.Record8;
31+
import org.jooq.Record9;
3232
import org.springframework.beans.factory.annotation.Autowired;
3333
import org.springframework.stereotype.Repository;
3434

@@ -39,6 +39,7 @@
3939
import static jooq.steve.db.tables.OcppTag.OCPP_TAG;
4040
import static jooq.steve.db.tables.SchemaVersion.SCHEMA_VERSION;
4141
import static jooq.steve.db.tables.User.USER;
42+
import static jooq.steve.db.tables.WebUser.WEB_USER;
4243
import static org.jooq.impl.DSL.max;
4344
import static org.jooq.impl.DSL.select;
4445

@@ -103,7 +104,12 @@ public Statistics getStats() {
103104
.where(date(CHARGE_BOX.LAST_HEARTBEAT_TIMESTAMP).lessThan(date(yesterdaysNow)))
104105
.asField("heartbeats_earlier");
105106

106-
Record8<Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer> gs =
107+
Field<Integer> numWebUsers =
108+
ctx.selectCount()
109+
.from(WEB_USER)
110+
.asField("num_webusers");
111+
112+
Record9<Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer> gs =
107113
ctx.select(
108114
numChargeBoxes,
109115
numOcppTags,
@@ -112,7 +118,8 @@ public Statistics getStats() {
112118
numTransactions,
113119
heartbeatsToday,
114120
heartbeatsYesterday,
115-
heartbeatsEarlier
121+
heartbeatsEarlier,
122+
numWebUsers
116123
).fetchOne();
117124

118125
return Statistics.builder()
@@ -124,6 +131,7 @@ public Statistics getStats() {
124131
.heartbeatToday(gs.value6())
125132
.heartbeatYesterday(gs.value7())
126133
.heartbeatEarlier(gs.value8())
134+
.numWebUsers(gs.value9())
127135
.build();
128136
}
129137

src/main/java/de/rwth/idsg/steve/repository/impl/WebUserRepositoryImpl.java

Lines changed: 83 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,12 @@
1818
*/
1919
package de.rwth.idsg.steve.repository.impl;
2020

21+
import com.fasterxml.jackson.core.JsonProcessingException;
22+
import com.fasterxml.jackson.databind.ObjectMapper;
2123
import de.rwth.idsg.steve.repository.WebUserRepository;
24+
import de.rwth.idsg.steve.repository.dto.WebUserOverview;
25+
import de.rwth.idsg.steve.web.dto.WebUserQueryForm;
26+
import java.util.List;
2227
import jooq.steve.db.tables.records.WebUserRecord;
2328
import lombok.RequiredArgsConstructor;
2429
import lombok.extern.slf4j.Slf4j;
@@ -27,6 +32,9 @@
2732
import org.springframework.stereotype.Repository;
2833

2934
import static jooq.steve.db.Tables.WEB_USER;
35+
import org.jooq.Record4;
36+
import org.jooq.Result;
37+
import org.jooq.SelectQuery;
3038
import static org.jooq.impl.DSL.condition;
3139
import static org.jooq.impl.DSL.count;
3240

@@ -40,6 +48,7 @@
4048
public class WebUserRepositoryImpl implements WebUserRepository {
4149

4250
private final DSLContext ctx;
51+
private final ObjectMapper jacksonObjectMapper;
4352

4453
@Override
4554
public void createUser(WebUserRecord user) {
@@ -53,12 +62,22 @@ public void createUser(WebUserRecord user) {
5362

5463
@Override
5564
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();
65+
// There is not alway the need to change the password
66+
if (user.getPassword().isBlank()) {
67+
ctx.update(WEB_USER)
68+
.set(WEB_USER.USERNAME, user.getUsername())
69+
.set(WEB_USER.ENABLED, user.getEnabled())
70+
.set(WEB_USER.AUTHORITIES, user.getAuthorities())
71+
.where(WEB_USER.USERNAME.eq(user.getUsername()))
72+
.execute();
73+
} else {
74+
ctx.update(WEB_USER)
75+
.set(WEB_USER.PASSWORD, user.getPassword())
76+
.set(WEB_USER.ENABLED, user.getEnabled())
77+
.set(WEB_USER.AUTHORITIES, user.getAuthorities())
78+
.where(WEB_USER.USERNAME.eq(user.getUsername()))
79+
.execute();
80+
}
6281
}
6382

6483
@Override
@@ -115,4 +134,61 @@ public WebUserRecord loadUserByUsername(String username) {
115134
.where(WEB_USER.USERNAME.eq(username))
116135
.fetchOne();
117136
}
118-
}
137+
138+
@Override
139+
public WebUserRecord loadUserByUsePk(Integer webUserPk) {
140+
return ctx.selectFrom(WEB_USER)
141+
.where(WEB_USER.WEB_USER_PK.eq(webUserPk))
142+
.fetchOne();
143+
}
144+
145+
@Override
146+
public List<WebUserOverview> getOverview(WebUserQueryForm form) {
147+
return getOverviewInternal(form)
148+
.map(r -> WebUserOverview.builder()
149+
.webUserPk(r.value1())
150+
.webusername(r.value2())
151+
.enabled(r.value3())
152+
.autorithies(fromJson(r.value4()))
153+
.build()
154+
);
155+
}
156+
157+
private String[] fromJson(JSON jsonArray) {
158+
try {
159+
return jacksonObjectMapper.readValue(jsonArray.data(), String[].class);
160+
} catch (JsonProcessingException e) {
161+
throw new RuntimeException(e);
162+
}
163+
}
164+
165+
@SuppressWarnings("unchecked")
166+
private Result<Record4<Integer, String, Boolean, JSON>> getOverviewInternal(WebUserQueryForm form) {
167+
SelectQuery selectQuery = ctx.selectQuery();
168+
selectQuery.addFrom(WEB_USER);
169+
selectQuery.addSelect(
170+
WEB_USER.WEB_USER_PK,
171+
WEB_USER.USERNAME,
172+
WEB_USER.ENABLED,
173+
WEB_USER.AUTHORITIES
174+
);
175+
176+
if (form.isSetWebusername()) {
177+
selectQuery.addConditions(WEB_USER.USERNAME.eq(form.getWebusername()));
178+
}
179+
180+
if (form.isSetEnabled()) {
181+
selectQuery.addConditions(WEB_USER.ENABLED.eq(form.getEnabled()));
182+
}
183+
184+
if (form.isSetRoles()) {
185+
String[] roles = form.getRoles().split(","); //Semicolon seperated String to StringArray
186+
for (String role : roles) {
187+
JSON authValue = JSON.json("\"" + role.strip() + "\"");
188+
selectQuery.addConditions(condition("json_contains({0}, {1})", WEB_USER.AUTHORITIES, authValue)); // strip--> No Withspace
189+
}
190+
}
191+
192+
return selectQuery.fetch();
193+
}
194+
}

src/main/java/de/rwth/idsg/steve/service/WebUserService.java

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,12 @@
2121
import com.fasterxml.jackson.core.JsonProcessingException;
2222
import com.fasterxml.jackson.databind.ObjectMapper;
2323
import de.rwth.idsg.steve.SteveConfiguration;
24+
import de.rwth.idsg.steve.SteveException;
2425
import de.rwth.idsg.steve.repository.WebUserRepository;
26+
import de.rwth.idsg.steve.repository.dto.WebUserOverview;
27+
import de.rwth.idsg.steve.web.dto.WebUserForm;
28+
import de.rwth.idsg.steve.web.dto.WebUserQueryForm;
29+
import java.util.ArrayList;
2530
import jooq.steve.db.tables.records.WebUserRecord;
2631
import lombok.RequiredArgsConstructor;
2732
import org.jooq.JSON;
@@ -42,10 +47,13 @@
4247

4348
import java.util.Collection;
4449
import java.util.LinkedHashSet;
50+
import java.util.List;
4551
import java.util.stream.Collectors;
4652

4753
import static org.springframework.security.authentication.UsernamePasswordAuthenticationToken.authenticated;
54+
import org.springframework.security.core.authority.SimpleGrantedAuthority;
4855
import static org.springframework.security.core.context.SecurityContextHolder.getContextHolderStrategy;
56+
import org.springframework.security.crypto.password.PasswordEncoder;
4957

5058
/**
5159
* Inspired by {@link org.springframework.security.provisioning.JdbcUserDetailsManager}
@@ -60,6 +68,7 @@ public class WebUserService implements UserDetailsManager {
6068
private final ObjectMapper jacksonObjectMapper;
6169
private final WebUserRepository webUserRepository;
6270
private final SecurityContextHolderStrategy securityContextHolderStrategy = getContextHolderStrategy();
71+
private final PasswordEncoder encoder;
6372

6473
@EventListener
6574
public void afterStart(ContextRefreshedEvent event) {
@@ -153,6 +162,53 @@ public boolean hasUserWithAuthority(String authority) {
153162
return count != null && count > 0;
154163
}
155164

165+
// Methods for the website
166+
public void add(WebUserForm form) {
167+
createUser(toUserDetails(form));
168+
}
169+
170+
171+
public void update(WebUserForm form) {
172+
updateUser(toUserDetails(form));
173+
}
174+
175+
public List<WebUserOverview> getOverview(WebUserQueryForm form) {
176+
return webUserRepository.getOverview(form);
177+
}
178+
179+
public WebUserForm getDetails(Integer webuserpk) {
180+
WebUserRecord ur = webUserRepository.loadUserByUsePk(webuserpk);
181+
182+
if (ur == null) {
183+
throw new SteveException("There is no user with id '%d'", webuserpk);
184+
}
185+
186+
WebUserForm form = new WebUserForm();
187+
188+
form.setEnabled(ur.getEnabled());
189+
form.setWebusername(ur.getUsername());
190+
form.setPassword(ur.getPassword());
191+
form.setApitoken(ur.getApiToken());
192+
form.setAuthorities(rolesStr(fromJson(ur.getAuthorities())));
193+
194+
return form;
195+
}
196+
197+
private static String rolesStr(String[] authorities) {
198+
String roles = "";
199+
200+
for (String ar : authorities) {
201+
roles = roles + ar + ", ";
202+
}
203+
roles = roles.strip();
204+
if (!roles.isBlank()) { //(roles.endsWith(","))
205+
roles = roles.substring(0, roles.length() - 1);
206+
}
207+
208+
return roles;
209+
}
210+
211+
// Helpers
156212
private WebUserRecord toWebUserRecord(UserDetails user) {
157213
return new WebUserRecord()
158214
.setUsername(user.getUsername())
@@ -161,6 +217,22 @@ private WebUserRecord toWebUserRecord(UserDetails user) {
161217
.setAuthorities(toJson(user.getAuthorities()));
162218
}
163219

220+
private UserDetails toUserDetails(WebUserForm form) {
221+
String encPw = "";
222+
if (form.getPassword()!= null) {
223+
//encPw = form.getPassword();
224+
encPw = encoder.encode(form.getPassword());
225+
}
226+
var user = User
227+
.withUsername(form.getWebusername())
228+
.password(encPw)
229+
.disabled(!form.getEnabled())
230+
.authorities(toAuthorities(form.getAuthorities()))
231+
.build();
232+
return user;
233+
}
234+
235+
164236
private String[] fromJson(JSON jsonArray) {
165237
try {
166238
return jacksonObjectMapper.readValue(jsonArray.data(), String[].class);
@@ -169,6 +241,15 @@ private String[] fromJson(JSON jsonArray) {
169241
}
170242
}
171243

244+
private Collection<? extends GrantedAuthority> toAuthorities(String authoritiesStr) {
245+
String[] authoritiesList = authoritiesStr.split(",");
246+
List<GrantedAuthority> authorities = new ArrayList<>();
247+
for (String authStr: authoritiesList) {
248+
authorities.add(new SimpleGrantedAuthority(authStr.strip()));
249+
}
250+
return authorities;
251+
}
252+
172253
private JSON toJson(Collection<? extends GrantedAuthority> authorities) {
173254
Collection<String> auths = authorities.stream()
174255
.map(GrantedAuthority::getAuthority)

0 commit comments

Comments
 (0)