diff --git a/shogun-config/src/main/java/de/terrestris/shogun/config/DefaultWebSecurityConfig.java b/shogun-config/src/main/java/de/terrestris/shogun/config/DefaultWebSecurityConfig.java index 55353539..1eae8c2b 100644 --- a/shogun-config/src/main/java/de/terrestris/shogun/config/DefaultWebSecurityConfig.java +++ b/shogun-config/src/main/java/de/terrestris/shogun/config/DefaultWebSecurityConfig.java @@ -59,18 +59,22 @@ default void customHttpConfiguration(HttpSecurity http) throws Exception { "/imagefiles", "/imagefiles/*" ) - .permitAll() + .permitAll() // Enable anonymous access to graphql (secured via permission evaluators) .requestMatchers( HttpMethod.POST, "/graphql" ) - .permitAll() + .permitAll() .requestMatchers( "/actuator/**", "/cache/**", "/webhooks/**", - "/ws/**" + "/ws/**", + // Explicitly require ADMIN role for the provider sync endpoints. + "/users/createAllFromProvider", + "/groups/createAllFromProvider", + "/roles/createAllFromProvider" ) .hasRole("ADMIN") .anyRequest() diff --git a/shogun-lib/src/main/java/de/terrestris/shogun/lib/controller/GroupController.java b/shogun-lib/src/main/java/de/terrestris/shogun/lib/controller/GroupController.java index 197d465c..6c756981 100644 --- a/shogun-lib/src/main/java/de/terrestris/shogun/lib/controller/GroupController.java +++ b/shogun-lib/src/main/java/de/terrestris/shogun/lib/controller/GroupController.java @@ -19,12 +19,16 @@ import de.terrestris.shogun.lib.model.Group; import de.terrestris.shogun.lib.model.User; import de.terrestris.shogun.lib.service.GroupService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; import io.swagger.v3.oas.annotations.security.SecurityRequirement; import io.swagger.v3.oas.annotations.tags.Tag; import lombok.extern.log4j.Log4j2; import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; import org.springframework.context.i18n.LocaleContextHolder; import org.springframework.http.HttpStatus; +import org.springframework.security.access.AccessDeniedException; import org.springframework.web.bind.annotation.*; import org.springframework.web.server.ResponseStatusException; @@ -36,8 +40,8 @@ @ConditionalOnExpression("${controller.groups.enabled:true}") @Log4j2 @Tag( - name = "Users", - description = "The endpoints to manage users" + name = "Groups", + description = "The endpoints to manage groups" ) @SecurityRequirement(name = "bearer-key") public class GroupController extends BaseController { @@ -93,4 +97,58 @@ public List getGroupMembers(@PathVariable("id") String keycloakId) { } } + @PostMapping("/createAllFromProvider") + @ResponseStatus(HttpStatus.NO_CONTENT) + @Operation( + summary = "Creates all groups from the associated group provider (usually Keycloak)", + security = { @SecurityRequirement(name = "bearer-key") } + ) + @ApiResponses(value = { + @ApiResponse( + responseCode = "204", + description = "No content: The groups were successfully created" + ), + @ApiResponse( + responseCode = "401", + description = "Unauthorized: You need to provide a bearer token" + ), + @ApiResponse( + responseCode = "403", + description = "Forbidden: You are not allowed to execute this operation (typically because of a " + + "missing ADMIN role)" + ), + @ApiResponse( + responseCode = "500", + description = "Internal Server Error: Something internal went wrong while creating the groups" + ) + }) + public void createAllFromProvider() { + log.trace("Requested to create all groups from the group provider"); + + try { + service.createAllFromProvider(); + + log.trace("Successfully created all groups from the group provider"); + } catch (AccessDeniedException ade) { + log.warn("Only users with ROLE_ADMIN are allowed to create groups from the group provider"); + log.trace("Full stack trace: ", ade); + + throw new ResponseStatusException( + HttpStatus.FORBIDDEN + ); + } catch (Exception e) { + log.error("Error while creating the groups from the group provider: \n {}", e.getMessage()); + log.trace("Full stack trace: ", e); + + throw new ResponseStatusException( + HttpStatus.INTERNAL_SERVER_ERROR, + messageSource.getMessage( + "BaseController.INTERNAL_SERVER_ERROR", + null, + LocaleContextHolder.getLocale() + ), + e + ); + } + } } diff --git a/shogun-lib/src/main/java/de/terrestris/shogun/lib/controller/RoleController.java b/shogun-lib/src/main/java/de/terrestris/shogun/lib/controller/RoleController.java index ca880641..35ca888f 100644 --- a/shogun-lib/src/main/java/de/terrestris/shogun/lib/controller/RoleController.java +++ b/shogun-lib/src/main/java/de/terrestris/shogun/lib/controller/RoleController.java @@ -18,18 +18,85 @@ import de.terrestris.shogun.lib.model.Role; import de.terrestris.shogun.lib.service.RoleService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; import io.swagger.v3.oas.annotations.security.SecurityRequirement; import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.extern.log4j.Log4j2; import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; +import org.springframework.context.i18n.LocaleContextHolder; +import org.springframework.http.HttpStatus; +import org.springframework.security.access.AccessDeniedException; +import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.server.ResponseStatusException; @RestController @RequestMapping("/roles") @ConditionalOnExpression("${controller.roles.enabled:true}") +@Log4j2 @Tag( name = "Roles", description = "The endpoints to manage roles" ) @SecurityRequirement(name = "bearer-key") -public class RoleController extends BaseController { } +public class RoleController extends BaseController { + + @PostMapping("/createAllFromProvider") + @ResponseStatus(HttpStatus.NO_CONTENT) + @Operation( + summary = "Creates all roles from the associated role provider (usually Keycloak)", + security = { @SecurityRequirement(name = "bearer-key") } + ) + @ApiResponses(value = { + @ApiResponse( + responseCode = "204", + description = "No content: The roles were successfully created" + ), + @ApiResponse( + responseCode = "401", + description = "Unauthorized: You need to provide a bearer token" + ), + @ApiResponse( + responseCode = "403", + description = "Forbidden: You are not allowed to execute this operation (typically because of a " + + "missing ADMIN role)" + ), + @ApiResponse( + responseCode = "500", + description = "Internal Server Error: Something internal went wrong while creating the roles" + ) + }) + public void createAllFromProvider() { + log.trace("Requested to create all roles from the role provider"); + + try { + service.createAllFromProvider(); + + log.trace("Successfully created all roles from the role provider"); + } catch (AccessDeniedException ade) { + log.warn("Only users with ROLE_ADMIN are allowed to create roles from the role provider"); + log.trace("Full stack trace: ", ade); + + throw new ResponseStatusException( + HttpStatus.FORBIDDEN + ); + } catch (Exception e) { + log.error("Error while creating the roles from the role provider: \n {}", e.getMessage()); + log.trace("Full stack trace: ", e); + + throw new ResponseStatusException( + HttpStatus.INTERNAL_SERVER_ERROR, + messageSource.getMessage( + "BaseController.INTERNAL_SERVER_ERROR", + null, + LocaleContextHolder.getLocale() + ), + e + ); + } + } +} diff --git a/shogun-lib/src/main/java/de/terrestris/shogun/lib/controller/UserController.java b/shogun-lib/src/main/java/de/terrestris/shogun/lib/controller/UserController.java index 2855426e..e09c09f7 100644 --- a/shogun-lib/src/main/java/de/terrestris/shogun/lib/controller/UserController.java +++ b/shogun-lib/src/main/java/de/terrestris/shogun/lib/controller/UserController.java @@ -18,18 +18,85 @@ import de.terrestris.shogun.lib.model.User; import de.terrestris.shogun.lib.service.UserService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; import io.swagger.v3.oas.annotations.security.SecurityRequirement; import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.extern.log4j.Log4j2; import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; +import org.springframework.context.i18n.LocaleContextHolder; +import org.springframework.http.HttpStatus; +import org.springframework.security.access.AccessDeniedException; +import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.server.ResponseStatusException; @RestController @RequestMapping("/users") @ConditionalOnExpression("${controller.users.enabled:true}") +@Log4j2 @Tag( name = "Users", description = "The endpoints to manage users" ) @SecurityRequirement(name = "bearer-key") -public class UserController extends BaseController { } +public class UserController extends BaseController { + + @PostMapping("/createAllFromProvider") + @ResponseStatus(HttpStatus.NO_CONTENT) + @Operation( + summary = "Creates all users from the associated user provider (usually Keycloak)", + security = { @SecurityRequirement(name = "bearer-key") } + ) + @ApiResponses(value = { + @ApiResponse( + responseCode = "204", + description = "No content: The users were successfully created" + ), + @ApiResponse( + responseCode = "401", + description = "Unauthorized: You need to provide a bearer token" + ), + @ApiResponse( + responseCode = "403", + description = "Forbidden: You are not allowed to execute this operation (typically because of a " + + "missing ADMIN role)" + ), + @ApiResponse( + responseCode = "500", + description = "Internal Server Error: Something internal went wrong while creating the users" + ) + }) + public void createAllFromProvider() { + log.trace("Requested to create all users from the user provider"); + + try { + service.createAllFromProvider(); + + log.trace("Successfully created all users from the user provider"); + } catch (AccessDeniedException ade) { + log.warn("Only users with ROLE_ADMIN are allowed to create users from the user provider"); + log.trace("Full stack trace: ", ade); + + throw new ResponseStatusException( + HttpStatus.FORBIDDEN + ); + } catch (Exception e) { + log.error("Error while creating the users from the user provider: \n {}", e.getMessage()); + log.trace("Full stack trace: ", e); + + throw new ResponseStatusException( + HttpStatus.INTERNAL_SERVER_ERROR, + messageSource.getMessage( + "BaseController.INTERNAL_SERVER_ERROR", + null, + LocaleContextHolder.getLocale() + ), + e + ); + } + } +} diff --git a/shogun-lib/src/main/java/de/terrestris/shogun/lib/listener/LoginListener.java b/shogun-lib/src/main/java/de/terrestris/shogun/lib/listener/LoginListener.java deleted file mode 100644 index d2d31bff..00000000 --- a/shogun-lib/src/main/java/de/terrestris/shogun/lib/listener/LoginListener.java +++ /dev/null @@ -1,52 +0,0 @@ -/* SHOGun, https://terrestris.github.io/shogun/ - * - * Copyright © 2020-present terrestris GmbH & Co. KG - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0.txt - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package de.terrestris.shogun.lib.listener; - -import de.terrestris.shogun.lib.service.security.provider.UserProviderService; -import de.terrestris.shogun.lib.util.KeycloakUtil; -import lombok.extern.log4j.Log4j2; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.context.ApplicationListener; -import org.springframework.security.authentication.event.InteractiveAuthenticationSuccessEvent; -import org.springframework.security.core.Authentication; -import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken; -import org.springframework.stereotype.Component; -import org.springframework.transaction.annotation.Transactional; - -@Component -@Log4j2 -public class LoginListener implements ApplicationListener { - - @Autowired - private UserProviderService userProviderService; - - @Override - @Transactional - public synchronized void onApplicationEvent(InteractiveAuthenticationSuccessEvent event) { - Authentication authentication = event.getAuthentication(); - - if (!(authentication instanceof JwtAuthenticationToken)) { - log.error("No JwtAuthenticationToken found, can not create the user."); - return; - } - - String keycloakUserId = KeycloakUtil.getKeycloakUserIdFromAuthentication(authentication); - - // Add missing user to shogun db - userProviderService.findOrCreateByProviderId(keycloakUserId); - } -} diff --git a/shogun-lib/src/main/java/de/terrestris/shogun/lib/service/GroupService.java b/shogun-lib/src/main/java/de/terrestris/shogun/lib/service/GroupService.java index 6bb8573d..afb59089 100644 --- a/shogun-lib/src/main/java/de/terrestris/shogun/lib/service/GroupService.java +++ b/shogun-lib/src/main/java/de/terrestris/shogun/lib/service/GroupService.java @@ -107,7 +107,7 @@ public List findByUser(User user) { } /** - * Delete a group from the SHOGun DB by its keycloak Id. + * Delete a group from the SHOGun DB by its keycloak Id. * * @param keycloakGroupId */ @@ -137,4 +137,15 @@ public void delete(Group group) { repository.delete(group); } + + /** + * Reads all groups from the configured group provider (usually Keycloak) and creates them in SHOGun. + * + * Note: Since the group provider service is not secured, this method just wraps it in a secured method and is + * used in the public HTTP REST API. + */ + @PreAuthorize("hasRole('ROLE_ADMIN')") + public void createAllFromProvider() { + groupProviderService.createAllGroups(); + } } diff --git a/shogun-lib/src/main/java/de/terrestris/shogun/lib/service/RoleService.java b/shogun-lib/src/main/java/de/terrestris/shogun/lib/service/RoleService.java index 1300c404..5f4f9faf 100644 --- a/shogun-lib/src/main/java/de/terrestris/shogun/lib/service/RoleService.java +++ b/shogun-lib/src/main/java/de/terrestris/shogun/lib/service/RoleService.java @@ -103,4 +103,15 @@ public void delete(Role role) { repository.delete(role); } + /** + * Reads all roles from the configured group provider (usually Keycloak) and creates them in SHOGun. + * + * Note: Since the role provider service is not secured, this method just wraps it in a secured method and is + * used in the public HTTP REST API. + */ + @PreAuthorize("hasRole('ROLE_ADMIN')") + public void createAllFromProvider() { + roleProviderService.createAllRoles(); + } + } diff --git a/shogun-lib/src/main/java/de/terrestris/shogun/lib/service/UserService.java b/shogun-lib/src/main/java/de/terrestris/shogun/lib/service/UserService.java index d67ca372..63cb15d0 100644 --- a/shogun-lib/src/main/java/de/terrestris/shogun/lib/service/UserService.java +++ b/shogun-lib/src/main/java/de/terrestris/shogun/lib/service/UserService.java @@ -123,4 +123,15 @@ public void delete(User user) { repository.delete(user); } + /** + * Reads all users from the configured group provider (usually Keycloak) and creates them in SHOGun. + * + * Note: Since the user provider service is not secured, this method just wraps it in a secured method and is + * used in the public HTTP REST API. + */ + @PreAuthorize("hasRole('ROLE_ADMIN')") + public void createAllFromProvider() { + userProviderService.createAllUsers(); + } + } diff --git a/shogun-lib/src/main/java/de/terrestris/shogun/lib/service/security/provider/GroupProviderService.java b/shogun-lib/src/main/java/de/terrestris/shogun/lib/service/security/provider/GroupProviderService.java index 4f4bd576..781afa44 100644 --- a/shogun-lib/src/main/java/de/terrestris/shogun/lib/service/security/provider/GroupProviderService.java +++ b/shogun-lib/src/main/java/de/terrestris/shogun/lib/service/security/provider/GroupProviderService.java @@ -33,4 +33,5 @@ public interface GroupProviderService { Group findOrCreateByProviderId(String providerGroupId); + void createAllGroups(); } diff --git a/shogun-lib/src/main/java/de/terrestris/shogun/lib/service/security/provider/RoleProviderService.java b/shogun-lib/src/main/java/de/terrestris/shogun/lib/service/security/provider/RoleProviderService.java index 0415e3d4..578803cd 100644 --- a/shogun-lib/src/main/java/de/terrestris/shogun/lib/service/security/provider/RoleProviderService.java +++ b/shogun-lib/src/main/java/de/terrestris/shogun/lib/service/security/provider/RoleProviderService.java @@ -28,4 +28,6 @@ public interface RoleProviderService { List getRolesForUser(User user); Role findOrCreateByProviderId(String providerRoleId); + + void createAllRoles(); } diff --git a/shogun-lib/src/main/java/de/terrestris/shogun/lib/service/security/provider/UserProviderService.java b/shogun-lib/src/main/java/de/terrestris/shogun/lib/service/security/provider/UserProviderService.java index dd180b11..b734654c 100644 --- a/shogun-lib/src/main/java/de/terrestris/shogun/lib/service/security/provider/UserProviderService.java +++ b/shogun-lib/src/main/java/de/terrestris/shogun/lib/service/security/provider/UserProviderService.java @@ -31,4 +31,5 @@ public interface UserProviderService { Optional> getUserFromAuthentication(Authentication authentication); + void createAllUsers(); } diff --git a/shogun-lib/src/main/java/de/terrestris/shogun/lib/service/security/provider/keycloak/KeycloakGroupProviderService.java b/shogun-lib/src/main/java/de/terrestris/shogun/lib/service/security/provider/keycloak/KeycloakGroupProviderService.java index 140742ee..312fbb87 100644 --- a/shogun-lib/src/main/java/de/terrestris/shogun/lib/service/security/provider/keycloak/KeycloakGroupProviderService.java +++ b/shogun-lib/src/main/java/de/terrestris/shogun/lib/service/security/provider/keycloak/KeycloakGroupProviderService.java @@ -151,4 +151,9 @@ public Group findOrCreateByProviderId(String keycloakGroupI return group; } + @Override + public void createAllGroups() { + var groupIds = keycloakUtil.getAllGroupIds(); + groupIds.forEach(this::findOrCreateByProviderId); + } } diff --git a/shogun-lib/src/main/java/de/terrestris/shogun/lib/service/security/provider/keycloak/KeycloakRoleProviderService.java b/shogun-lib/src/main/java/de/terrestris/shogun/lib/service/security/provider/keycloak/KeycloakRoleProviderService.java index 422777d0..68428884 100644 --- a/shogun-lib/src/main/java/de/terrestris/shogun/lib/service/security/provider/keycloak/KeycloakRoleProviderService.java +++ b/shogun-lib/src/main/java/de/terrestris/shogun/lib/service/security/provider/keycloak/KeycloakRoleProviderService.java @@ -96,4 +96,11 @@ public Role findOrCreateByProviderId(String providerRoleId) return role; } + + @Override + public void createAllRoles() { + var clientRoles = keycloakUtil.getClientRoles(); + var clientRolesIds = clientRoles.stream().map(RoleRepresentation::getId).toList(); + clientRolesIds.forEach(this::findOrCreateByProviderId); + } } diff --git a/shogun-lib/src/main/java/de/terrestris/shogun/lib/service/security/provider/keycloak/KeycloakUserProviderService.java b/shogun-lib/src/main/java/de/terrestris/shogun/lib/service/security/provider/keycloak/KeycloakUserProviderService.java index ad38c63b..22d8ec76 100644 --- a/shogun-lib/src/main/java/de/terrestris/shogun/lib/service/security/provider/keycloak/KeycloakUserProviderService.java +++ b/shogun-lib/src/main/java/de/terrestris/shogun/lib/service/security/provider/keycloak/KeycloakUserProviderService.java @@ -159,4 +159,9 @@ public Optional> getUserFromAuthentication(Authenticati return (Optional) userRepository.findByAuthProviderId(keycloakUserId); } + @Override + public void createAllUsers() { + var userIds = keycloakUtil.getAllUserIds(); + userIds.forEach(this::findOrCreateByProviderId); + } } diff --git a/shogun-lib/src/main/java/de/terrestris/shogun/lib/util/KeycloakUtil.java b/shogun-lib/src/main/java/de/terrestris/shogun/lib/util/KeycloakUtil.java index 06ea3326..f92e4364 100644 --- a/shogun-lib/src/main/java/de/terrestris/shogun/lib/util/KeycloakUtil.java +++ b/shogun-lib/src/main/java/de/terrestris/shogun/lib/util/KeycloakUtil.java @@ -28,6 +28,7 @@ import org.keycloak.admin.client.resource.*; import org.keycloak.representations.IDToken; import org.keycloak.representations.idm.ClientRepresentation; +import org.keycloak.representations.idm.AbstractUserRepresentation; import org.keycloak.representations.idm.GroupRepresentation; import org.keycloak.representations.idm.RoleRepresentation; import org.keycloak.representations.idm.UserRepresentation; @@ -71,6 +72,11 @@ public UserResource getUserResource(String id) { return kcUsers.get(id); } + public List getAllUserIds() { + UsersResource kcUsers = this.keycloakRealm.users(); + return kcUsers.list().stream().map(AbstractUserRepresentation::getId).toList(); + } + public GroupResource getGroupResource(Group group) { GroupsResource kcGroups = this.keycloakRealm.groups(); return kcGroups.group(group.getAuthProviderId()); @@ -81,6 +87,11 @@ public GroupResource getGroupResource(String id) { return kcGroups.group(id); } + public List getAllGroupIds() { + GroupsResource kcGroups = this.keycloakRealm.groups(); + return kcGroups.groups().stream().map(GroupRepresentation::getId).toList(); + } + public void addUserToGroup(User user, Group group) { UserResource kcUser = this.getUserResource(user); GroupResource kcGroup = this.getGroupResource(group);