diff --git a/pom.xml b/pom.xml index 398bad29e..42e55e769 100644 --- a/pom.xml +++ b/pom.xml @@ -632,6 +632,11 @@ org.springframework spring-websocket + + org.springframework + spring-jdbc + jar + org.springframework.security spring-security-web diff --git a/src/main/java/de/rwth/idsg/steve/config/BeanConfiguration.java b/src/main/java/de/rwth/idsg/steve/config/BeanConfiguration.java index fb6abcf19..f754a2aab 100644 --- a/src/main/java/de/rwth/idsg/steve/config/BeanConfiguration.java +++ b/src/main/java/de/rwth/idsg/steve/config/BeanConfiguration.java @@ -89,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(); @@ -111,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); } /** @@ -128,7 +129,9 @@ private void initDataSource() { */ @Bean public DSLContext dslContext() { - initDataSource(); + if (dataSource == null) { + dataSource = dataSource(); + } Settings settings = new Settings() // Normally, the records are "attached" to the Configuration that created (i.e. fetch/insert) them. diff --git a/src/main/java/de/rwth/idsg/steve/config/SecurityConfiguration.java b/src/main/java/de/rwth/idsg/steve/config/SecurityConfiguration.java index 31da890be..253003bf7 100644 --- a/src/main/java/de/rwth/idsg/steve/config/SecurityConfiguration.java +++ b/src/main/java/de/rwth/idsg/steve/config/SecurityConfiguration.java @@ -18,8 +18,10 @@ */ package de.rwth.idsg.steve.config; +import static de.rwth.idsg.steve.SteveConfiguration.CONFIG; import com.fasterxml.jackson.databind.ObjectMapper; 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; @@ -33,17 +35,10 @@ import org.springframework.security.authentication.DisabledException; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; -import org.springframework.security.config.annotation.web.configuration.WebSecurityCustomizer; 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; @@ -53,7 +48,11 @@ import jakarta.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; +import org.springframework.security.web.util.matcher.AntPathRequestMatcher; /** * @author Sevket Goekay @@ -65,6 +64,10 @@ @Conditional(SteveProdCondition.class) public class SecurityConfiguration { + @Autowired + private HikariDataSource dataSource; + + /** * 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. @@ -77,17 +80,6 @@ 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 SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { final String prefix = CONFIG.getSpringManagerMapping(); @@ -95,12 +87,35 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti return http .authorizeHttpRequests( req -> req - .requestMatchers( - "/static/**", - CONFIG.getCxfMapping() + "/**", - "/WEB-INF/views/**" // https://github.com/spring-projects/spring-security/issues/13285#issuecomment-1579097065 - ).permitAll() + // https://github.com/spring-projects/spring-security/issues/13285#issuecomment-1579097065 + .requestMatchers(new AntPathRequestMatcher("/static/**")).permitAll() + .requestMatchers(new AntPathRequestMatcher(CONFIG.getCxfMapping() + "/**")).permitAll() + .requestMatchers(new AntPathRequestMatcher("/WEB-INF/views/**")).permitAll() + .requestMatchers(prefix + "/**").hasRole("ADMIN") + .requestMatchers(new AntPathRequestMatcher(prefix + "/home")).hasAnyRole("USER", "ADMIN") + // webuser + .requestMatchers(new AntPathRequestMatcher(prefix + "/webusers")).hasAnyRole("USER", "ADMIN") + .requestMatchers(new AntPathRequestMatcher(prefix + "/webusers" + "/details/**")).hasAnyRole("USER", "ADMIN") + // users + .requestMatchers(new AntPathRequestMatcher(prefix + "/users")).hasAnyRole("USER", "ADMIN") + .requestMatchers(new AntPathRequestMatcher(prefix + "/users" + "/details/**")).hasAnyRole("USER", "ADMIN") + //ocppTags + .requestMatchers(new AntPathRequestMatcher(prefix + "/ocppTags")).hasAnyRole("USER", "ADMIN") + .requestMatchers(new AntPathRequestMatcher(prefix + "/ocppTags" + "/details/**")).hasAnyRole("USER", "ADMIN") + // chargepoints + .requestMatchers(new AntPathRequestMatcher(prefix + "/chargepoints")).hasAnyRole("USER", "ADMIN") + .requestMatchers(new AntPathRequestMatcher(prefix + "/chargepoints" + "/details/**")).hasAnyRole("USER", "ADMIN") + // transactions and reservations + .requestMatchers(new AntPathRequestMatcher(prefix + "/transactions")).hasAnyRole("USER", "ADMIN") + .requestMatchers(new AntPathRequestMatcher(prefix + "/transactions" + "/details/**")).hasAnyRole("USER", "ADMIN") + .requestMatchers(new AntPathRequestMatcher(prefix + "/reservations")).hasAnyRole("USER", "ADMIN") + .requestMatchers(new AntPathRequestMatcher(prefix + "/reservations" + "/**")).hasRole("ADMIN") + // singout and noAccess + .requestMatchers(new AntPathRequestMatcher(prefix + "/signout/" + "**")).hasAnyRole("USER", "ADMIN") + .requestMatchers(new AntPathRequestMatcher(prefix + "/noAccess/" + "**")).hasAnyRole("USER", "ADMIN") + // any other site + .requestMatchers(new AntPathRequestMatcher(prefix + "/**")).hasRole("ADMIN") ) .sessionManagement( req -> req.invalidSessionUrl(prefix + "/signin") @@ -111,9 +126,65 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti .logout( 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); + // 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 { diff --git a/src/main/java/de/rwth/idsg/steve/repository/WebUserRepository.java b/src/main/java/de/rwth/idsg/steve/repository/WebUserRepository.java new file mode 100644 index 000000000..a74abb511 --- /dev/null +++ b/src/main/java/de/rwth/idsg/steve/repository/WebUserRepository.java @@ -0,0 +1,39 @@ +/* + * 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 . + */ +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 fnkbsi + * @since 21.03.2022 + */ +public interface WebUserRepository { + List 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); +} diff --git a/src/main/java/de/rwth/idsg/steve/repository/dto/WebUser.java b/src/main/java/de/rwth/idsg/steve/repository/dto/WebUser.java new file mode 100644 index 000000000..ae459f43a --- /dev/null +++ b/src/main/java/de/rwth/idsg/steve/repository/dto/WebUser.java @@ -0,0 +1,46 @@ +/* + * 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 . + */ +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 fnkbsi + * @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 webauthoritiesRecordList; + } +} diff --git a/src/main/java/de/rwth/idsg/steve/repository/impl/GenericRepositoryImpl.java b/src/main/java/de/rwth/idsg/steve/repository/impl/GenericRepositoryImpl.java index 1c458b059..81ca960ee 100644 --- a/src/main/java/de/rwth/idsg/steve/repository/impl/GenericRepositoryImpl.java +++ b/src/main/java/de/rwth/idsg/steve/repository/impl/GenericRepositoryImpl.java @@ -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; @@ -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.Webusers.WEBUSERS; import static org.jooq.impl.DSL.max; import static org.jooq.impl.DSL.select; @@ -103,7 +104,12 @@ public Statistics getStats() { .where(date(CHARGE_BOX.LAST_HEARTBEAT_TIMESTAMP).lessThan(date(yesterdaysNow))) .asField("heartbeats_earlier"); - Record8 gs = + Field numWebUsers = + ctx.selectCount() + .from(WEBUSERS) + .asField("num_webusers"); + + Record9 gs = ctx.select( numChargeBoxes, numOcppTags, @@ -112,7 +118,8 @@ public Statistics getStats() { numTransactions, heartbeatsToday, heartbeatsYesterday, - heartbeatsEarlier + heartbeatsEarlier, + numWebUsers ).fetchOne(); return Statistics.builder() @@ -124,6 +131,7 @@ public Statistics getStats() { .heartbeatToday(gs.value6()) .heartbeatYesterday(gs.value7()) .heartbeatEarlier(gs.value8()) + .numWebUsers(gs.value9()) .build(); } diff --git a/src/main/java/de/rwth/idsg/steve/repository/impl/WebUserRepositoryImpl.java b/src/main/java/de/rwth/idsg/steve/repository/impl/WebUserRepositoryImpl.java new file mode 100644 index 000000000..0dff010aa --- /dev/null +++ b/src/main/java/de/rwth/idsg/steve/repository/impl/WebUserRepositoryImpl.java @@ -0,0 +1,279 @@ +/* + * 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 . + */ +package de.rwth.idsg.steve.repository.impl; + +import de.rwth.idsg.steve.SteveException; +import de.rwth.idsg.steve.repository.WebUserRepository; +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 jooq.steve.db.tables.records.WebauthoritiesRecord; +import jooq.steve.db.tables.records.WebusersRecord; +import lombok.extern.slf4j.Slf4j; +import org.jooq.DSLContext; +import org.jooq.JoinType; +import org.jooq.Record3; +import org.jooq.Result; +import org.jooq.SelectQuery; +import org.jooq.exception.DataAccessException; +import org.jooq.impl.DSL; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Repository; + +import java.util.List; + +import static de.rwth.idsg.steve.utils.CustomDSL.includes; +import static jooq.steve.db.tables.Webusers.WEBUSERS; +import static jooq.steve.db.tables.Webauthorities.WEBAUTHORITIES; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; + + + /** + * @author fnkbsi + * @since 01.04.2022 + */ +@Slf4j +@Repository +public class WebUserRepositoryImpl implements WebUserRepository { + + @Autowired private DSLContext ctx; + + private PasswordEncoder encoder = new BCryptPasswordEncoder(); + + @Override + public List getOverview(WebUserQueryForm form) { + return getOverviewInternal(form) + .map(r -> WebUser.Overview.builder() + .webusername(r.value1()) + .enabled(r.value2()) + .roles(r.value3()) + .build() + ); + } + + + @Override + public WebUser.Details getDetails(String webusername) { + + // ------------------------------------------------------------------------- + // 1. user table + // ------------------------------------------------------------------------- + + WebusersRecord ur = ctx.selectFrom(WEBUSERS) + .where(WEBUSERS.USERNAME.equal(webusername)) + .fetchOne(); + + if (ur == null) { + throw new SteveException("There is no user with id '%s'", webusername); + } + + // ------------------------------------------------------------------------- + // 2. address table + // ------------------------------------------------------------------------- + + List lar = ctx.selectFrom(WEBAUTHORITIES) + .where(WEBAUTHORITIES.USERNAME.eq(webusername)) + .fetch(); + + if (lar == null) { + throw new SteveException("There is no role for user '%s' defined", webusername); + } + + // ------------------------------------------------------------------------- + // 3. ocpp_tag table + // ------------------------------------------------------------------------- + + return WebUser.Details.builder() + .webusersRecord(ur) + .webauthoritiesRecordList(lar) + .build(); + } + + @Override + public void add(WebUserForm form) { + ctx.transaction(configuration -> { + DSLContext ctx = DSL.using(configuration); + try { + addInternal(ctx, form); + } catch (DataAccessException e) { + throw new SteveException("Failed to add the user", e); + } + }); + } + + @Override + public void update(WebUserForm form) { + ctx.transaction(configuration -> { + DSLContext ctx = DSL.using(configuration); + try { + updateInternal(ctx, form); + + } catch (DataAccessException e) { + throw new SteveException("Failed to update the webuser", e); + } + }); + } + + @Override + public void delete(String webusername, String role) { + ctx.transaction(configuration -> { + DSLContext ctx = DSL.using(configuration); + try { + deleteInternal(ctx, webusername, role); + + } catch (DataAccessException e) { + throw new SteveException("Failed to delete the webuser", e); + } + }); + } + + @Override + public void delete(String webusername) { + ctx.transaction(configuration -> { + DSLContext ctx = DSL.using(configuration); + try { + deleteInternal(ctx, webusername); + } catch (DataAccessException e) { + throw new SteveException("Failed to delete the webuser", e); + } + }); + } + + // ------------------------------------------------------------------------- + // Private helpers + // ------------------------------------------------------------------------- + + @SuppressWarnings("unchecked") + private Result> getOverviewInternal(WebUserQueryForm form) { + SelectQuery selectQuery = ctx.selectQuery(); + selectQuery.addFrom(WEBUSERS); + selectQuery.addJoin(WEBAUTHORITIES, JoinType.LEFT_OUTER_JOIN, WEBUSERS.USERNAME.eq(WEBAUTHORITIES.USERNAME)); + selectQuery.addSelect( + WEBUSERS.USERNAME, + WEBUSERS.ENABLED, + WEBAUTHORITIES.AUTHORITY + ); + + if (form.isSetWebusername()) { + selectQuery.addConditions(WEBUSERS.USERNAME.eq(form.getWebusername())); + } + + if (form.isSetEnabled()) { + selectQuery.addConditions(WEBUSERS.ENABLED.eq(form.getEnabled())); + } + + if (form.isSetRoles()) { + String[] roles = form.getRoles().split(";"); //Semicolon seperated String to StringArray + for (String role : roles) { + selectQuery.addConditions(includes(WEBAUTHORITIES.AUTHORITY, role.strip())); // strip--> No Withspace + } + } + + return selectQuery.fetch(); + } + + + private void addInternal(DSLContext ctx, WebUserForm form) { + int count = 0; + + count = ctx.insertInto(WEBUSERS) + .set(WEBUSERS.USERNAME, form.getWebusername()) + .set(WEBUSERS.ENABLED, form.getEnabled()) + .set(WEBUSERS.PASSWORD, encoder.encode(form.getPassword())) + .execute(); + + String[] roles = form.getRoles().split(";"); //Semicolon seperated String to StringArray + for (String role : roles) { + count += ctx.insertInto(WEBAUTHORITIES) + .set(WEBAUTHORITIES.USERNAME, form.getWebusername()) + .set(WEBAUTHORITIES.AUTHORITY, role.strip()) + .execute(); + } + + if (count == 0) { + throw new SteveException("Failed to insert the user"); + } + } + + private void updateInternal(DSLContext ctx, WebUserForm form) { + if (form.getPassword() == null) { + ctx.update(WEBUSERS) + .set(WEBUSERS.USERNAME, form.getWebusername()) + .set(WEBUSERS.ENABLED, form.getEnabled()) + //set Username unnessary until WebUserForm has oldName or the table and the form uses a primary key + .where(WEBUSERS.USERNAME.eq(form.getWebusername())) + .execute(); + } else { + ctx.update(WEBUSERS) + .set(WEBUSERS.USERNAME, form.getWebusername()) + .set(WEBUSERS.ENABLED, form.getEnabled()) + .set(WEBUSERS.PASSWORD, encoder.encode(form.getPassword())) + //set Username unnessary until WebUserForm has oldName or the table and the form uses a primary key + .where(WEBUSERS.USERNAME.eq(form.getWebusername())) + .execute(); + } + // delete all Authority entries for this webuser + ctx.delete(WEBAUTHORITIES) + .where(WEBAUTHORITIES.USERNAME.equal(form.getWebusername())) + .execute(); + + String[] roles = form.getRoles().split(";"); //Semicolon seperated String to StringArray + for (String role : roles) { + ctx.insertInto(WEBAUTHORITIES) + .set(WEBAUTHORITIES.USERNAME, form.getWebusername()) + .set(WEBAUTHORITIES.AUTHORITY, role.strip()) + .execute(); + } + } + + private void deleteInternal(DSLContext ctx, String webusername, String role) { + + ctx.delete(WEBAUTHORITIES) + .where(WEBAUTHORITIES.USERNAME.equal(webusername)) + .and(WEBAUTHORITIES.AUTHORITY.eq(role)) + .execute(); + + boolean isEmpty = ctx.selectFrom(WEBAUTHORITIES) + .where(WEBAUTHORITIES.USERNAME.equal(webusername)) + .fetch().isEmpty(); + + if (isEmpty) { + ctx.delete(WEBUSERS) + .where(WEBUSERS.USERNAME.equal(webusername)) + .execute(); + } + + } + + private void deleteInternal(DSLContext ctx, String webusername) { + + Integer count = ctx.selectCount().from(WEBUSERS).execute(); + + if (count > 1) { // don't delete the last webuser! + ctx.delete(WEBAUTHORITIES) + .where(WEBAUTHORITIES.USERNAME.equal(webusername)) + .execute(); + + ctx.delete(WEBUSERS) + .where(WEBUSERS.USERNAME.equal(webusername)) + .execute(); + } + } +} diff --git a/src/main/java/de/rwth/idsg/steve/utils/mapper/WebUserFormMapper.java b/src/main/java/de/rwth/idsg/steve/utils/mapper/WebUserFormMapper.java new file mode 100644 index 000000000..172bb71dc --- /dev/null +++ b/src/main/java/de/rwth/idsg/steve/utils/mapper/WebUserFormMapper.java @@ -0,0 +1,62 @@ +/* + * 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 . + */ +package de.rwth.idsg.steve.utils.mapper; + +import de.rwth.idsg.steve.repository.dto.WebUser; +import de.rwth.idsg.steve.web.dto.WebUserForm; +import java.util.List; +import jooq.steve.db.tables.records.WebusersRecord; +import jooq.steve.db.tables.records.WebauthoritiesRecord; +import lombok.AccessLevel; +import lombok.NoArgsConstructor; + + /** + * @author fnkbsi + * @since 01.04.2022 + */ +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public final class WebUserFormMapper { + + public static WebUserForm toForm(WebUser.Details details) { + WebusersRecord webuserRecord = details.getWebusersRecord(); + List authRecords = details.getWebauthoritiesRecordList(); + + WebUserForm form = new WebUserForm(); + form.setWebusername(webuserRecord.getUsername()); + form.setEnabled(webuserRecord.getEnabled()); + + form.setRoles(rolesStr(authRecords)); + + return form; + } + + private static String rolesStr(List authRecords) { + String roles = ""; + + for (WebauthoritiesRecord ar : authRecords) { + roles = roles + ar.getAuthority() + "; "; + } + roles = roles.strip(); + if (!roles.isBlank()) { //(roles.endsWith(";")) + roles = roles.substring(0, roles.length() - 1); + } + + return roles; + } +} diff --git a/src/main/java/de/rwth/idsg/steve/web/controller/NoAccessController.java b/src/main/java/de/rwth/idsg/steve/web/controller/NoAccessController.java new file mode 100644 index 000000000..3cecc0906 --- /dev/null +++ b/src/main/java/de/rwth/idsg/steve/web/controller/NoAccessController.java @@ -0,0 +1,45 @@ +/* + * 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 . + */ +package de.rwth.idsg.steve.web.controller; + + + +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.RequestMapping; + + + + /** + * @author fnkbsi + * @since 01.04.2022 + */ +@Controller +@RequestMapping(value = "/manager/noAccess") +public class NoAccessController { + + // ------------------------------------------------------------------------- + // HTTP methods + // ------------------------------------------------------------------------- + + @RequestMapping() + public String accessDenied() { + return "noAccess"; + } + +} diff --git a/src/main/java/de/rwth/idsg/steve/web/controller/WebUsersController.java b/src/main/java/de/rwth/idsg/steve/web/controller/WebUsersController.java new file mode 100644 index 000000000..2345a8bed --- /dev/null +++ b/src/main/java/de/rwth/idsg/steve/web/controller/WebUsersController.java @@ -0,0 +1,183 @@ +/* + * 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 . + */ +package de.rwth.idsg.steve.web.controller; + + +import de.rwth.idsg.steve.repository.WebUserRepository; +import de.rwth.idsg.steve.repository.dto.WebUser; +import de.rwth.idsg.steve.utils.mapper.WebUserFormMapper; +import de.rwth.idsg.steve.web.dto.WebUserForm; +import de.rwth.idsg.steve.web.dto.WebUserQueryForm; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Controller; +import org.springframework.ui.Model; +import org.springframework.validation.BindingResult; +import org.springframework.web.bind.annotation.ModelAttribute; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; + +import jakarta.validation.Valid; + + /** + * @author fnkbsi + * @since 01.04.2022 + */ +@Controller +@RequestMapping(value = "/manager/webusers") +public class WebUsersController { + + @Autowired private WebUserRepository webuserRepository; + + private static final String PARAMS = "params"; + + // ------------------------------------------------------------------------- + // Paths + // ------------------------------------------------------------------------- + + private static final String QUERY_PATH = "/query"; + + private static final String DETAILS_PATH = "/details/{webusername}"; + private static final String DELETE_PATH = "/delete/{webusername}/{role}"; + private static final String DELETE_ALL_PATH = "/delete/{webusername}"; + private static final String UPDATE_PATH = "/update"; + private static final String ADD_PATH = "/add"; + + // ------------------------------------------------------------------------- + // HTTP methods + // ------------------------------------------------------------------------- + + @RequestMapping(method = RequestMethod.GET) + public String getOverview(Model model) { + initList(model, new WebUserQueryForm()); + return "data-man/webusers"; + } + + @RequestMapping(value = QUERY_PATH, method = RequestMethod.GET) + public String getQuery(@ModelAttribute(PARAMS) WebUserQueryForm params, Model model) { + initList(model, params); + return "data-man/webusers"; + } + + private void initList(Model model, WebUserQueryForm params) { + model.addAttribute(PARAMS, params); + model.addAttribute("webuserList", webuserRepository.getOverview(params)); + } + + @RequestMapping(value = DETAILS_PATH, method = RequestMethod.GET) + public String getDetails(@PathVariable("webusername") String webusername, Model model) { + WebUser.Details details = webuserRepository.getDetails(webusername); + WebUserForm form = WebUserFormMapper.toForm(details); + + model.addAttribute("webuserForm", form); + return "data-man/webuserDetails"; + } + + @RequestMapping(value = ADD_PATH, method = RequestMethod.GET) + public String addGet(Model model) { + model.addAttribute("webuserForm", new WebUserForm()); + return "data-man/webuserAdd"; + } + + @RequestMapping(params = "add", value = ADD_PATH, method = RequestMethod.POST) + public String addPost(@Valid @ModelAttribute("webuserForm") WebUserForm webuserForm, + BindingResult result, Model model) { + if (result.hasErrors()) { + return "data-man/webuserAdd"; + } + + // password is Null, Blank, Empty or less than 8 Characters then don't add and show an Error + if (webuserForm.getPassword() == null) { + webuserForm.setPwerror(Boolean.TRUE); + return "data-man/webuserAdd"; + + } + + if ((webuserForm.getPassword().length() < 8) | webuserForm.getPassword().isBlank()) + /* | webuserForm.getPassword().isEmpty() in isBlank included */ { + webuserForm.setPwerror(Boolean.TRUE); + return "data-man/webuserAdd"; + } + + // Compare both the password inputs + if (!webuserForm.getPassword().equals(webuserForm.getPasswordComparison())) { + webuserForm.setPwerror(Boolean.TRUE); + return "data-man/webuserAdd"; + } + + webuserForm.setPwerror(Boolean.FALSE); + + webuserRepository.add(webuserForm); + return toOverview(); + } + + @RequestMapping(params = "update", value = UPDATE_PATH, method = RequestMethod.POST) + public String update(@Valid @ModelAttribute("webuserForm") WebUserForm webuserForm, + BindingResult result, Model model) { + if (result.hasErrors()) { + return "data-man/webuserDetails"; + } + + if (webuserForm.getPassword() != null) { + if (!webuserForm.getPassword().equals(webuserForm.getPasswordComparison())) { + webuserForm.setPwerror(Boolean.TRUE); + return "data-man/webuserDetails"; + } + // password is Blank or less than 8 Characters then don't update and show an Error + // --> WebUserRepositoryImpl: Null and Empty update without updating the password + if (webuserForm.getPassword().isBlank() | webuserForm.getPassword().length() < 8) { + webuserForm.setPwerror(Boolean.TRUE); + return "data-man/webuserDetails"; + } + } + + webuserRepository.update(webuserForm); + return toOverview(); + } + + @RequestMapping(value = DELETE_PATH, method = RequestMethod.POST) + public String delete(@PathVariable("webusername") String webusername, @PathVariable("role") String role) { + webuserRepository.delete(webusername, role); + return toOverview(); + } + + @RequestMapping(value = DELETE_ALL_PATH, method = RequestMethod.POST) + public String delete(@PathVariable("webusername") String webusername) { + webuserRepository.delete(webusername); + return toOverview(); + } + + // ------------------------------------------------------------------------- + // Back to Overview + // ------------------------------------------------------------------------- + + @RequestMapping(params = "backToOverview", value = ADD_PATH, method = RequestMethod.POST) + public String addBackToOverview() { + return toOverview(); + } + + @RequestMapping(params = "backToOverview", value = UPDATE_PATH, method = RequestMethod.POST) + public String updateBackToOverview() { + return toOverview(); + } + + private String toOverview() { + return "redirect:/manager/webusers"; + } +} diff --git a/src/main/java/de/rwth/idsg/steve/web/dto/Statistics.java b/src/main/java/de/rwth/idsg/steve/web/dto/Statistics.java index 9f2293c89..48eef3408 100644 --- a/src/main/java/de/rwth/idsg/steve/web/dto/Statistics.java +++ b/src/main/java/de/rwth/idsg/steve/web/dto/Statistics.java @@ -37,7 +37,9 @@ public final class Statistics { // Number of chargeboxes, ocppTags, users, reservations, transactions private final Integer numChargeBoxes, numOcppTags, numUsers, numReservations, numTransactions, // Received heartbeats - heartbeatToday, heartbeatYesterday, heartbeatEarlier; + heartbeatToday, heartbeatYesterday, heartbeatEarlier, + //WebUser + numWebUsers; // Number of connected WebSocket/JSON chargeboxes @Setter private int numOcpp12JChargeBoxes, numOcpp15JChargeBoxes, numOcpp16JChargeBoxes; diff --git a/src/main/java/de/rwth/idsg/steve/web/dto/WebUserForm.java b/src/main/java/de/rwth/idsg/steve/web/dto/WebUserForm.java new file mode 100644 index 000000000..5f2beb974 --- /dev/null +++ b/src/main/java/de/rwth/idsg/steve/web/dto/WebUserForm.java @@ -0,0 +1,45 @@ +/* + * 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 . + */ +package de.rwth.idsg.steve.web.dto; + +import lombok.Getter; +import lombok.Setter; + + + /** + * @author fnkbsi + * @since 01.04.2022 + */ +@Getter +@Setter +public class WebUserForm { + + // Internal database id + private Boolean enabled; + + private String webusername; + + private String password; + + private String passwordComparison; + + private String roles; + + private Boolean pwerror; +} diff --git a/src/main/java/de/rwth/idsg/steve/web/dto/WebUserQueryForm.java b/src/main/java/de/rwth/idsg/steve/web/dto/WebUserQueryForm.java new file mode 100644 index 000000000..d8e3b5cbb --- /dev/null +++ b/src/main/java/de/rwth/idsg/steve/web/dto/WebUserQueryForm.java @@ -0,0 +1,53 @@ +/* + * SteVe - SteckdosenVerwaltung - https://github.com/steve-community/steve + * Copyright (C) 2013-2023 SteVe Community Team +/* + * 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 . + */ +package de.rwth.idsg.steve.web.dto; + +import lombok.Getter; +import lombok.Setter; + + /** + * @author fnkbsi + * @since 01.04.2022 + */ +@Getter +@Setter +public class WebUserQueryForm { + + private Boolean enabled; + + // Free text input + private String webusername; + private String roles; + + public boolean isSetWebusername() { + return webusername != null; + } + + public boolean isSetRoles() { + return roles != null; + } + + public boolean isSetEnabled() { + return enabled != null; + } + +} diff --git a/src/main/resources/db/migration/V1_0_6__update.sql b/src/main/resources/db/migration/V1_0_6__update.sql new file mode 100644 index 000000000..1a5cf08df --- /dev/null +++ b/src/main/resources/db/migration/V1_0_6__update.sql @@ -0,0 +1,29 @@ +-- adding tables for multi users access + +CREATE TABLE `webusers` ( + `username` varchar(50) COLLATE utf8mb3_unicode_ci NOT NULL, + `password` varchar(255) COLLATE utf8mb3_unicode_ci NOT NULL, + `enabled` tinyint(1) NOT NULL, + PRIMARY KEY (`username`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_unicode_ci; + +CREATE TABLE `webauthorities` ( + `username` varchar(50) COLLATE utf8mb3_unicode_ci NOT NULL, + `authority` varchar(50) COLLATE utf8mb3_unicode_ci NOT NULL DEFAULT 'ROLE_USER', + UNIQUE KEY `authorities_idx_1` (`username`,`authority`), + CONSTRAINT `authorities_ibfk_1` FOREIGN KEY (`username`) REFERENCES `webusers` (`username`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_unicode_ci; + +CREATE UNIQUE INDEX ix_auth_username + on webauthorities (username,authority); + +-- Insert a user = admin with the password = pass. Change password after installing! +INSERT INTO webusers (username, password, enabled) + values ('admin', + '$2a$10$.Rxx4JnuX8OGJTIOCXn76euuB3dIGHHrkX9tswYt9ECKjAGyms30W', + 1); + +INSERT INTO webauthorities (username, authority) + values ('admin', 'ROLE_ADMIN'); + + \ No newline at end of file diff --git a/src/main/resources/webapp/WEB-INF/views/00-header.jsp b/src/main/resources/webapp/WEB-INF/views/00-header.jsp index fcccd7e38..b64be4f46 100644 --- a/src/main/resources/webapp/WEB-INF/views/00-header.jsp +++ b/src/main/resources/webapp/WEB-INF/views/00-header.jsp @@ -27,8 +27,10 @@ <%@ include file="00-context.jsp" %> - + + + @@ -45,7 +47,7 @@
- + Steve-Logo
@@ -57,6 +59,7 @@
  • CHARGE POINTS
  • OCPP TAGS
  • USERS
  • +
  • WEBUSERS
  • CHARGING PROFILES
  • RESERVATIONS
  • TRANSACTIONS
  • diff --git a/src/main/resources/webapp/WEB-INF/views/data-man/webuserAdd.jsp b/src/main/resources/webapp/WEB-INF/views/data-man/webuserAdd.jsp new file mode 100644 index 000000000..a86b60dd1 --- /dev/null +++ b/src/main/resources/webapp/WEB-INF/views/data-man/webuserAdd.jsp @@ -0,0 +1,72 @@ +<%-- + + 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 . + +--%> +<%@ include file="../00-header.jsp" %> + + +
    + Error while trying to add a webuser: +
      + +
    • ${error.defaultMessage}
    • +
      +
    +
    +
    + +
    The password is to short or the password input is not identical.
    +
    +
    +
    Add Webuser
    + + + + + + + + + + + + + + + +
    New Webuser
    Webusername:
    Password:
    Password confirmation:
    Roles: +
    +
    +
    Enabled:true
    + + + + +
    +
    +
    +<%@ include file="../00-footer.jsp" %> \ No newline at end of file diff --git a/src/main/resources/webapp/WEB-INF/views/data-man/webuserDetails.jsp b/src/main/resources/webapp/WEB-INF/views/data-man/webuserDetails.jsp new file mode 100644 index 000000000..68b5f515e --- /dev/null +++ b/src/main/resources/webapp/WEB-INF/views/data-man/webuserDetails.jsp @@ -0,0 +1,80 @@ +<%-- + + 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 . + +--%> +<%@ include file="../00-header.jsp" %> + +
    + Error while trying to update a charge point: +
      + +
    • ${error.defaultMessage}
    • +
      +
    +
    +
    + +
    The password is to short or the password input is not identical.
    +
    +
    +
    Webuser Details
    + + + + + + + + + + + + + + + + + + + +
    Webuser
    Username:${webusername}
    Password:
    Password confirmation:
    Roles: + +
    Enabled: + +
    + + + + + + +
    +
    +
    +<%@ include file="../00-footer.jsp" %> + diff --git a/src/main/resources/webapp/WEB-INF/views/data-man/webusers.jsp b/src/main/resources/webapp/WEB-INF/views/data-man/webusers.jsp new file mode 100644 index 000000000..eedf69bbd --- /dev/null +++ b/src/main/resources/webapp/WEB-INF/views/data-man/webusers.jsp @@ -0,0 +1,72 @@ +<%-- + + 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 . + +--%> +<%@ include file="../00-header.jsp" %> + +
    +
    Webuser Overview
    + + + + + + + + + + +
    Username:
    + +
    +
    +
    + + + + + + + + + + + + + + + + + + +
    UsernameRolesEnabled + + + +
    ${cr.webusername}${cr.roles}${cr.enabled} + + + +
    +
    +<%@ include file="../00-footer.jsp" %> \ No newline at end of file diff --git a/src/main/resources/webapp/WEB-INF/views/home.jsp b/src/main/resources/webapp/WEB-INF/views/home.jsp index 0a4c6fbad..6efc3021c 100644 --- a/src/main/resources/webapp/WEB-INF/views/home.jsp +++ b/src/main/resources/webapp/WEB-INF/views/home.jsp @@ -33,6 +33,10 @@ Number of
    Users ${stats.numUsers} + + Number of
    Webusers + ${stats.numWebUsers} +
    Number of
    Active Reservations ${stats.numReservations} diff --git a/src/main/resources/webapp/WEB-INF/views/noAccess.jsp b/src/main/resources/webapp/WEB-INF/views/noAccess.jsp new file mode 100644 index 000000000..ae6c0970d --- /dev/null +++ b/src/main/resources/webapp/WEB-INF/views/noAccess.jsp @@ -0,0 +1,41 @@ +<%-- + + 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 . + +--%> + +<%@ include file="00-header.jsp" %> +<%@ include file="00-context.jsp" %> + +
    +<%@ include file="00-footer.jsp" %> \ No newline at end of file diff --git a/src/main/resources/webapp/WEB-INF/views/signin.jsp b/src/main/resources/webapp/WEB-INF/views/signin.jsp index 27aa43993..5e41a3563 100644 --- a/src/main/resources/webapp/WEB-INF/views/signin.jsp +++ b/src/main/resources/webapp/WEB-INF/views/signin.jsp @@ -24,16 +24,18 @@ <%@ include file="00-context.jsp" %> - + + + - + SteVe - Steckdosenverwaltung
    -
    +
    Steve-Logo