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

Added website stuff for Web-User administration and additional request… #1544

Open
wants to merge 22 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
22 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
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
24 changes: 24 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 @@ -86,6 +86,27 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti
"/WEB-INF/views/**" // https://github.com/spring-projects/spring-security/issues/13285#issuecomment-1579097065
).permitAll()
.requestMatchers(prefix + "/**").hasAuthority("ADMIN")
.requestMatchers(prefix + "/home").hasAnyAuthority("USER", "ADMIN")
// webuser
.requestMatchers(prefix + "/webusers").hasAnyAuthority("USER", "ADMIN")
.requestMatchers(prefix + "/webusers" + "/details/**").hasAnyAuthority("USER", "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")
Copy link
Contributor

Choose a reason for hiding this comment

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

As user can't create, update ou delete charpoints (and some others), the UI should be updated (at least remove delete and update button)

Copy link
Member

Choose a reason for hiding this comment

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

As user can't create, update ou delete charpoints (and some others)

i will play devil's advocate (as setup for my next argument): why not?

your argument has a very specific understanding/model of what a user and admin should do or do not. i find it really hard to make decisions in steve about permissions and roles matrix because each customer/CPO/company would have different requirements depending on their organisation structure.

i have no solution for this. i dont think we can make some decisions (in this generic version of steve) that will make people happy.

Copy link
Member

Choose a reason for hiding this comment

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

the perfectionist in me says.. we should make the permissions and roles configurable in web UI or something, but this would be a big undertaking.

Copy link
Contributor

Choose a reason for hiding this comment

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

Sure, why not?

Let me share my own experience where people (administrative persons but technical operators too) unintentionally break things because they can.

But what if we could give them a way to only "read" data and help drivers?

At the same time, "users" could be drivers in different contexts.

In this vision, users become helpful companions for drivers/them, providing information and simple commands that won't cause any damage. Meanwhile, admins will be in charge of managing the network of stations.

Now, the question arises: how would we differentiate between admins and users? Can you imagine any noticeable differences in the way they are currently implemented?

To the perfectionist: I think it is recommended that implementation of peripheral functionalities, such as the one in question or even IAM, be delegated to specialized software stacks. This ensures that the primary objective of the software solution remains the central focus, while benefiting from the seamless integration of a well-established stack that effectively addresses the requirement.
However, it is important to note that implementing this way will require additional effort and may introduce complexity to the existing system. This could potentially impact the ops part.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Actually the button are still there, but USER landing on the NoAccess site. If someone with more *jsp experience can hide them, I would welcome this!

Copy link
Contributor

Choose a reason for hiding this comment

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

I can do some ui improvments but @geokay must define first what is expected for USER and ADMIN.

Copy link
Contributor

Choose a reason for hiding this comment

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

Another option can be ADMIN, SUPERVISOR, USER.
With
USER = readonly, except users,
SUPERVISOR = read+write, except users,
ADMIN = read+write all

Wdyt @goekay ? Please advice.

Copy link
Member

Choose a reason for hiding this comment

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

i wrote this in an earlier comment, and i still think that way: probably, we should not make any decisions about this.

we can create the basic framework (i.e. the preparation for decisions) and ability to create roles. what these roles can do and cannot do should be left to each user of steve.

there are so many aspects and functionalities, i.e. master data management (read and write) and various ocpp operations. i find it hard to come up with one matrix that will be the solution.

otherwise, we will get questions/issues like: why cant my USER role do X?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

In the current state of the code creating roles is possible by adapting the enum.
Changing the privileges can be done by adapting the SecurityFilterChain, which of course could be tricky (#1544 (comment)).
Are comments in the source code sufficient?

So for the basic framework solution configuration I suggest:
User --> allowed to see all website/data
Admin --> allowed to see all websites and is allowed to change the data
[User, Admin] is commented out in the enum. I would like to keep it as a hint that multiple roles are technically possible

.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")
)
// SOAP stations are making POST calls for communication. even though the following path is permitted for
// all access, there is a global default behaviour from spring security: enable CSRF for all POSTs.
Expand All @@ -100,6 +121,9 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti
.logout(
req -> req.logoutUrl(prefix + "/signout")
)
.exceptionHandling(
req -> req.accessDeniedPage(prefix + "/noAccess")
)
.build();
}

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

import jooq.steve.db.tables.records.WebUserRecord;
import de.rwth.idsg.steve.repository.dto.WebUserOverview;
import de.rwth.idsg.steve.web.dto.WebUserQueryForm;
import java.util.List;

public interface WebUserRepository {

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

boolean userExists(String username);

WebUserRecord loadUserByUsePk(Integer webUserPk);
WebUserRecord loadUserByUsername(String username);
}

List<WebUserOverview> getOverview(WebUserQueryForm form);

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/*
* SteVe - SteckdosenVerwaltung - https://github.com/steve-community/steve
* Copyright (C) 2013-2024 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 lombok.Builder;
import lombok.Getter;

@Getter
@Builder
public final class WebUserOverview {

private final Integer webUserPk;
private final Boolean enabled;
private final String webusername;
fnkbsi marked this conversation as resolved.
Show resolved Hide resolved
private final String[] autorithies;
goekay marked this conversation as resolved.
Show resolved Hide resolved
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
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;

Expand All @@ -39,6 +39,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 @@ -103,7 +104,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 @@ -112,7 +118,8 @@ public Statistics getStats() {
numTransactions,
heartbeatsToday,
heartbeatsYesterday,
heartbeatsEarlier
heartbeatsEarlier,
numWebUsers
).fetchOne();

return Statistics.builder()
Expand All @@ -124,6 +131,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 @@ -18,7 +18,12 @@
*/
package de.rwth.idsg.steve.repository.impl;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import de.rwth.idsg.steve.repository.WebUserRepository;
import de.rwth.idsg.steve.repository.dto.WebUserOverview;
import de.rwth.idsg.steve.web.dto.WebUserQueryForm;
import java.util.List;
import jooq.steve.db.tables.records.WebUserRecord;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
Expand All @@ -27,6 +32,9 @@
import org.springframework.stereotype.Repository;

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

Expand All @@ -40,6 +48,7 @@
public class WebUserRepositoryImpl implements WebUserRepository {

private final DSLContext ctx;
private final ObjectMapper jacksonObjectMapper;
fnkbsi marked this conversation as resolved.
Show resolved Hide resolved

@Override
public void createUser(WebUserRecord user) {
Expand All @@ -53,12 +62,22 @@

@Override
public void updateUser(WebUserRecord user) {
ctx.update(WEB_USER)
.set(WEB_USER.PASSWORD, user.getPassword())
.set(WEB_USER.ENABLED, user.getEnabled())
.set(WEB_USER.AUTHORITIES, user.getAuthorities())
.where(WEB_USER.USERNAME.eq(user.getUsername()))
.execute();
// There is not alway the need to change the password
fnkbsi marked this conversation as resolved.
Show resolved Hide resolved
if (user.getPassword().isBlank()) {
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.USERNAME.eq(user.getUsername()))
.execute();
} else {
ctx.update(WEB_USER)
.set(WEB_USER.PASSWORD, user.getPassword())
.set(WEB_USER.ENABLED, user.getEnabled())
.set(WEB_USER.AUTHORITIES, user.getAuthorities())
.where(WEB_USER.USERNAME.eq(user.getUsername()))
.execute();
}
}

@Override
Expand Down Expand Up @@ -115,4 +134,61 @@
.where(WEB_USER.USERNAME.eq(username))
.fetchOne();
}
}

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

@Override
public List<WebUserOverview> getOverview(WebUserQueryForm form) {
return getOverviewInternal(form)
.map(r -> WebUserOverview.builder()
.webUserPk(r.value1())
.webusername(r.value2())
.enabled(r.value3())
.autorithies(fromJson(r.value4()))
.build()
);
}

private String[] fromJson(JSON jsonArray) {
try {
return jacksonObjectMapper.readValue(jsonArray.data(), String[].class);
} catch (JsonProcessingException e) {
throw new RuntimeException(e);
}
}

@SuppressWarnings("unchecked")
private Result<Record4<Integer, String, Boolean, JSON>> getOverviewInternal(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[] roles = form.getRoles().split(","); //Semicolon seperated String to StringArray
for (String role : roles) {
JSON authValue = JSON.json("\"" + role.strip() + "\"");
selectQuery.addConditions(condition("json_contains({0}, {1})", WEB_USER.AUTHORITIES, authValue)); // strip--> No Withspace

Check failure on line 188 in src/main/java/de/rwth/idsg/steve/repository/impl/WebUserRepositoryImpl.java

View workflow job for this annotation

GitHub Actions / checkstyle

[checkstyle] reported by reviewdog 🐶 Line is longer than 120 characters (found 138). Raw Output: /github/workspace/./src/main/java/de/rwth/idsg/steve/repository/impl/WebUserRepositoryImpl.java:188:0: error: Line is longer than 120 characters (found 138). (com.puppycrawl.tools.checkstyle.checks.sizes.LineLengthCheck)
}
}

return selectQuery.fetch();
}
}
81 changes: 81 additions & 0 deletions src/main/java/de/rwth/idsg/steve/service/WebUserService.java
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,12 @@
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import de.rwth.idsg.steve.SteveConfiguration;
import de.rwth.idsg.steve.SteveException;
import de.rwth.idsg.steve.repository.WebUserRepository;
import de.rwth.idsg.steve.repository.dto.WebUserOverview;
import de.rwth.idsg.steve.web.dto.WebUserForm;
import de.rwth.idsg.steve.web.dto.WebUserQueryForm;
import java.util.ArrayList;
import jooq.steve.db.tables.records.WebUserRecord;
import lombok.RequiredArgsConstructor;
import org.jooq.JSON;
Expand All @@ -42,10 +47,13 @@

import java.util.Collection;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.stream.Collectors;

import static org.springframework.security.authentication.UsernamePasswordAuthenticationToken.authenticated;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import static org.springframework.security.core.context.SecurityContextHolder.getContextHolderStrategy;
import org.springframework.security.crypto.password.PasswordEncoder;

/**
* Inspired by {@link org.springframework.security.provisioning.JdbcUserDetailsManager}
Expand All @@ -60,6 +68,7 @@
private final ObjectMapper jacksonObjectMapper;
private final WebUserRepository webUserRepository;
private final SecurityContextHolderStrategy securityContextHolderStrategy = getContextHolderStrategy();
private final PasswordEncoder encoder;

@EventListener
public void afterStart(ContextRefreshedEvent event) {
Expand Down Expand Up @@ -153,6 +162,53 @@
return count != null && count > 0;
}

// Methods for the website
public void add(WebUserForm form) {
createUser(toUserDetails(form));
}


public void update(WebUserForm form) {
updateUser(toUserDetails(form));
}

public List<WebUserOverview> getOverview(WebUserQueryForm form) {
return webUserRepository.getOverview(form);
}

public WebUserForm getDetails(Integer webuserpk) {
fnkbsi marked this conversation as resolved.
Show resolved Hide resolved
WebUserRecord ur = webUserRepository.loadUserByUsePk(webuserpk);

if (ur == null) {
throw new SteveException("There is no user with id '%d'", webuserpk);
}

WebUserForm form = new WebUserForm();

form.setEnabled(ur.getEnabled());
form.setWebusername(ur.getUsername());
form.setPassword(ur.getPassword());
fnkbsi marked this conversation as resolved.
Show resolved Hide resolved
form.setApitoken(ur.getApiToken());
form.setAuthorities(rolesStr(fromJson(ur.getAuthorities())));

return form;
}

private static String rolesStr(String[] authorities) {
String roles = "";

for (String ar : authorities) {
roles = roles + ar + ", ";
}
roles = roles.strip();
if (!roles.isBlank()) { //(roles.endsWith(","))
roles = roles.substring(0, roles.length() - 1);
}

return roles;
}

// Helpers
private WebUserRecord toWebUserRecord(UserDetails user) {
return new WebUserRecord()
.setUsername(user.getUsername())
Expand All @@ -161,6 +217,22 @@
.setAuthorities(toJson(user.getAuthorities()));
}

private UserDetails toUserDetails(WebUserForm form) {
String encPw = "";
if (form.getPassword()!= null) {

Check failure on line 222 in src/main/java/de/rwth/idsg/steve/service/WebUserService.java

View workflow job for this annotation

GitHub Actions / checkstyle

[checkstyle] reported by reviewdog 🐶 '!=' is not preceded with whitespace. Raw Output: /github/workspace/./src/main/java/de/rwth/idsg/steve/service/WebUserService.java:222:31: error: '!=' is not preceded with whitespace. (com.puppycrawl.tools.checkstyle.checks.whitespace.WhitespaceAroundCheck)
//encPw = form.getPassword();
encPw = encoder.encode(form.getPassword());
}
var user = User
.withUsername(form.getWebusername())
.password(encPw)
.disabled(!form.getEnabled())
.authorities(toAuthorities(form.getAuthorities()))
.build();
return user;
}


private String[] fromJson(JSON jsonArray) {
try {
return jacksonObjectMapper.readValue(jsonArray.data(), String[].class);
Expand All @@ -169,6 +241,15 @@
}
}

private Collection<? extends GrantedAuthority> toAuthorities(String authoritiesStr) {
String[] authoritiesList = authoritiesStr.split(",");
List<GrantedAuthority> authorities = new ArrayList<>();
for (String authStr: authoritiesList) {
authorities.add(new SimpleGrantedAuthority(authStr.strip()));
}
return authorities;
}

private JSON toJson(Collection<? extends GrantedAuthority> authorities) {
Collection<String> auths = authorities.stream()
.map(GrantedAuthority::getAuthority)
Expand Down
Loading