Skip to content
Merged
Show file tree
Hide file tree
Changes from 28 commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
cb5af53
Added website stuff for Web-User administarion and additional request…
fnkbsi Aug 16, 2024
4344489
delete Statistics.java.bak
Aug 17, 2024
30d2e78
WebUserForm, WebUserQueryForm use camel case
Aug 17, 2024
2fa85d7
WebUserOverview moved to service/dto, use camel case and correct typo
Aug 17, 2024
2bfa311
WebUserForm use camel case at webUsername
Aug 17, 2024
e2f7b0f
webuser*.jsp: adapt to the using of camel case in the dto's
Aug 17, 2024
a605705
moved the mapping part of List<WebUserOverview> getOverview(WebUserQu…
fnkbsi Aug 17, 2024
88d2880
WebUserController: using camel case
fnkbsi Aug 17, 2024
b9b43be
WebUserServive don't expose pw and apitoken to ui
fnkbsi Aug 17, 2024
d00c287
refactor
mrge-sevket-goekay Aug 17, 2024
56015ba
WebUserOverview: changed String[] authorities to String
Aug 18, 2024
8aa37f1
Adding Enum WebUserAuthority and adapt the code using it
Aug 18, 2024
7f5ab25
BugFix: Set the ".requestMatchers(prefix + "/**").hasAuthority("ADMIN…
fnkbsi Aug 28, 2024
3936817
WebUserController removed password comparison; webuserAdd.jsp, webuse…
fnkbsi Aug 28, 2024
e13f60a
Merge branch 'master' into MultiUser_4
fnkbsi Aug 28, 2024
d7a2db8
GenericRepositoryImpl: re-add 'import org.jooq.impl.DSL;' (wrong deci…
fnkbsi Aug 28, 2024
3ee9c65
resolve style checks
fnkbsi Aug 28, 2024
6e78297
Merge branch 'steve-community:master' into MultiUser_4
fnkbsi Oct 9, 2024
fd0d461
WebUserForm.java: pwError changed from AssertTrue to AssertFalse
fnkbsi Oct 9, 2024
bfbe877
Improvements on webUser password change. Only own password can be cha…
fnkbsi Oct 9, 2024
d77afa3
add website to set and change WebUser API password
fnkbsi Nov 7, 2024
3d4eaa4
Merge branch 'steve-community:master' into MultiUser_4
fnkbsi Dec 24, 2024
9b2aee4
Merge branch 'steve-community:master' into MultiUser_4
fnkbsi Feb 25, 2025
661500b
Merge branch 'steve-community:master' into MultiUser_4
fnkbsi Apr 5, 2025
c89df4b
Merge remote-tracking branch 'fnkbsi/MultiUser_4' into upstream
juherr Aug 15, 2025
498caf3
fix: update after CodeRabbit review
juherr Aug 18, 2025
2edfbee
fix: update licence
juherr Aug 18, 2025
3a7b344
fix: json array elements may have different sort
juherr Aug 18, 2025
a845ce9
fix: update after Copilot review
juherr Aug 18, 2025
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
39 changes: 39 additions & 0 deletions src/main/java/de/rwth/idsg/steve/config/SecurityConfiguration.java
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,9 @@
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;

import static de.rwth.idsg.steve.SteveConfiguration.CONFIG;
import org.springframework.http.HttpMethod;
import org.springframework.security.web.access.expression.WebExpressionAuthorizationManager;
import org.springframework.security.web.util.matcher.RequestMatcher;

/**
* @author Sevket Goekay <sevketgokay@gmail.com>
Expand Down Expand Up @@ -60,6 +63,11 @@ public PasswordEncoder passwordEncoder() {
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
final String prefix = CONFIG.getSpringManagerMapping();

RequestMatcher toOverview = request -> {
String param = request.getParameter("backToOverview");
return param != null && !param.isEmpty();
};

return http
.authorizeHttpRequests(
req -> req
Expand All @@ -69,6 +77,34 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti
WebSocketConfiguration.PATH_INFIX + "**",
"/WEB-INF/views/**" // https://github.com/spring-projects/spring-security/issues/13285#issuecomment-1579097065
).permitAll()
.requestMatchers(prefix + "/home").hasAnyAuthority("USER", "ADMIN")
// webuser
//only allowed to change the own password
.requestMatchers(prefix + "/webusers/password/{name}")
.access(new WebExpressionAuthorizationManager("#name == authentication.name"))
.requestMatchers(prefix + "/webusers/apipassword/{name}")
.access(new WebExpressionAuthorizationManager("#name == authentication.name"))
// otherwise denies access on backToOverview!
.requestMatchers(toOverview).hasAnyAuthority("USER", "ADMIN")
.requestMatchers(HttpMethod.GET, prefix + "/webusers/**").hasAnyAuthority("USER", "ADMIN")
.requestMatchers(HttpMethod.POST, prefix + "/webusers/**").hasAuthority("ADMIN")
// users
.requestMatchers(prefix + "/users").hasAnyAuthority("USER", "ADMIN")
.requestMatchers(prefix + "/users/details/**").hasAnyAuthority("USER", "ADMIN")
//ocppTags
.requestMatchers(prefix + "/ocppTags").hasAnyAuthority("USER", "ADMIN")
.requestMatchers(prefix + "/ocppTags/details/**").hasAnyAuthority("USER", "ADMIN")
// chargepoints
.requestMatchers(prefix + "/chargepoints").hasAnyAuthority("USER", "ADMIN")
.requestMatchers(prefix + "/chargepoints/details/**").hasAnyAuthority("USER", "ADMIN")
// transactions and reservations
.requestMatchers(prefix + "/transactions").hasAnyAuthority("USER", "ADMIN")
.requestMatchers(prefix + "/transactions/details/**").hasAnyAuthority("USER", "ADMIN")
.requestMatchers(prefix + "/reservations").hasAnyAuthority("USER", "ADMIN")
.requestMatchers(prefix + "/reservations/**").hasAnyAuthority("ADMIN")
// singout and noAccess
.requestMatchers(prefix + "/signout/**").hasAnyAuthority("USER", "ADMIN")
.requestMatchers(prefix + "/noAccess/**").hasAnyAuthority("USER", "ADMIN")
.requestMatchers(prefix + "/**").hasAuthority("ADMIN")
)
// SOAP stations are making POST calls for communication. even though the following path is permitted for
Expand All @@ -84,6 +120,9 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti
.logout(
req -> req.logoutUrl(prefix + "/signout")
)
.exceptionHandling(
req -> req.accessDeniedPage(prefix + "/noAccess")
)
.build();
}

Expand Down
14 changes: 14 additions & 0 deletions src/main/java/de/rwth/idsg/steve/repository/WebUserRepository.java
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,20 @@
*/
package de.rwth.idsg.steve.repository;

import de.rwth.idsg.steve.web.dto.WebUserQueryForm;
import jooq.steve.db.tables.records.WebUserRecord;
import org.jooq.JSON;
import org.jooq.Record4;
import org.jooq.Result;

public interface WebUserRepository {

void createUser(WebUserRecord user);

void updateUser(WebUserRecord user);

void updateUserByPk(WebUserRecord user);

void deleteUser(String username);

void deleteUser(int webUserPk);
Expand All @@ -36,7 +42,15 @@ public interface WebUserRepository {

void changePassword(String username, String newPassword);

void changePassword(Integer userPk, String newPassword);

void changeApiPassword(Integer userPk, String newPassword);

boolean userExists(String username);

WebUserRecord loadUserByUserPk(Integer webUserPk);

WebUserRecord loadUserByUsername(String username);

Result<Record4<Integer, String, Boolean, JSON>> getOverview(WebUserQueryForm form);
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
import org.jooq.DatePart;
import org.jooq.Field;
import org.jooq.Record2;
import org.jooq.Record8;
import org.jooq.Record9;
import org.jooq.impl.DSL;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.event.ContextRefreshedEvent;
Expand All @@ -46,6 +46,7 @@
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.WebUser.WEB_USER;
import static org.jooq.impl.DSL.max;
import static org.jooq.impl.DSL.select;

Expand Down Expand Up @@ -129,7 +130,12 @@ public Statistics getStats() {
.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(WEB_USER)
.asField("num_webusers");

Record9<Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer> gs =
ctx.select(
numChargeBoxes,
numOcppTags,
Expand All @@ -138,7 +144,8 @@ public Statistics getStats() {
numTransactions,
heartbeatsToday,
heartbeatsYesterday,
heartbeatsEarlier
heartbeatsEarlier,
numWebUsers
).fetchOne();

return Statistics.builder()
Expand All @@ -150,6 +157,7 @@ public Statistics getStats() {
.heartbeatToday(gs.value6())
.heartbeatYesterday(gs.value7())
.heartbeatEarlier(gs.value8())
.numWebUsers(gs.value9())
.build();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,17 +19,29 @@
package de.rwth.idsg.steve.repository.impl;

import de.rwth.idsg.steve.repository.WebUserRepository;
import de.rwth.idsg.steve.web.dto.WebUserQueryForm;
import jooq.steve.db.tables.records.WebUserRecord;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.jooq.Condition;
import org.jooq.DSLContext;
import org.jooq.Field;
import org.jooq.JSON;
import org.jooq.Record4;
import org.jooq.Result;
import org.jooq.SelectQuery;
import org.jooq.impl.DSL;
import org.jooq.impl.SQLDataType;
import org.springframework.stereotype.Repository;

import static jooq.steve.db.Tables.WEB_USER;
import static org.jooq.impl.DSL.condition;
import static org.jooq.impl.DSL.count;

import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Objects;

/**
* @author Sevket Goekay <sevketgokay@gmail.com>
* @since 10.08.2024
Expand All @@ -54,15 +66,25 @@ public void createUser(WebUserRecord user) {

@Override
public void updateUser(WebUserRecord user) {
// To change the password use one of the changePassword methods
ctx.update(WEB_USER)
.set(WEB_USER.PASSWORD, user.getPassword())
.set(WEB_USER.API_PASSWORD, user.getApiPassword())
.set(WEB_USER.ENABLED, user.getEnabled())
.set(WEB_USER.AUTHORITIES, user.getAuthorities())
.where(WEB_USER.USERNAME.eq(user.getUsername()))
.execute();
}

@Override
public void updateUserByPk(WebUserRecord user) {
// To change the password use one of the changePassword methods
ctx.update(WEB_USER)
.set(WEB_USER.USERNAME, user.getUsername())
.set(WEB_USER.ENABLED, user.getEnabled())
.set(WEB_USER.AUTHORITIES, user.getAuthorities())
.where(WEB_USER.WEB_USER_PK.eq(user.getWebUserPk()))
.execute();
}

@Override
public void deleteUser(String username) {
ctx.delete(WEB_USER)
Expand All @@ -87,10 +109,9 @@ public void changeStatusOfUser(String username, boolean enabled) {

@Override
public Integer getUserCountWithAuthority(String authority) {
JSON authValue = JSON.json("\"" + authority + "\"");
return ctx.selectCount()
.from(WEB_USER)
.where(condition("json_contains({0}, {1})", WEB_USER.AUTHORITIES, authValue))
.where(conditionsForAuthorities(Collections.singletonList(authority)))
.fetchOne(count());
}

Expand All @@ -102,6 +123,22 @@ public void changePassword(String username, String newPassword) {
.execute();
}

@Override
public void changePassword(Integer userPk, String newPassword) {
ctx.update(WEB_USER)
.set(WEB_USER.PASSWORD, newPassword)
.where(WEB_USER.WEB_USER_PK.eq(userPk))
.execute();
}

@Override
public void changeApiPassword(Integer userPk, String newPassword) {
ctx.update(WEB_USER)
.set(WEB_USER.API_PASSWORD, newPassword)
.where(WEB_USER.WEB_USER_PK.eq(userPk))
.execute();
}

@Override
public boolean userExists(String username) {
return ctx.selectOne()
Expand All @@ -117,4 +154,52 @@ public WebUserRecord loadUserByUsername(String username) {
.where(WEB_USER.USERNAME.eq(username))
.fetchOne();
}

@Override
public WebUserRecord loadUserByUserPk(Integer webUserPk) {
return ctx.selectFrom(WEB_USER)
.where(WEB_USER.WEB_USER_PK.eq(webUserPk))
.fetchOne();
}

@Override
public Result<Record4<Integer, String, Boolean, JSON>> getOverview(WebUserQueryForm form) {
SelectQuery selectQuery = ctx.selectQuery();
selectQuery.addFrom(WEB_USER);
selectQuery.addSelect(
WEB_USER.WEB_USER_PK,
WEB_USER.USERNAME,
WEB_USER.ENABLED,
WEB_USER.AUTHORITIES
);

if (form.isSetWebUsername()) {
selectQuery.addConditions(WEB_USER.USERNAME.eq(form.getWebUsername()));
}

if (form.isSetEnabled()) {
selectQuery.addConditions(WEB_USER.ENABLED.eq(form.getEnabled()));
}

if (form.isSetRoles()) {
String[] split = form.getRoles().split(","); // Comma seperated String to StringArray
List<String> roles = Arrays.stream(split).map(String::strip).toList();
selectQuery.addConditions(conditionsForAuthorities(roles));
}

return selectQuery.fetch();
}

private static List<Condition> conditionsForAuthorities(List<String> authorities) {
return authorities.stream()
.filter(Objects::nonNull)
.filter(it -> !it.trim().isEmpty())
.map(WebUserRepositoryImpl::jsonQuote)
.map(WEB_USER.AUTHORITIES::contains)
.toList();
}

private static Field<JSON> jsonQuote(String element) {
return DSL.field("JSON_QUOTE({0})", SQLDataType.JSON, DSL.val(element));
}
}
Loading
Loading