Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
772 changes: 772 additions & 0 deletions docs/AWS_SETUP_AND_RESOURCE_GUIDE.md

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -31,16 +31,13 @@
*/
@Slf4j
@Component
@RequiredArgsConstructor
public class JwtAuthenticationFilter extends OncePerRequestFilter {

private final JwtService jwtService;

@Autowired(required = false) // RedisTemplate이 필수가 아님을 명시
private RedisTemplate<String, Object> redisTemplate;

public JwtAuthenticationFilter(JwtService jwtService) {
this.jwtService = jwtService;
}

/**
* 필터 내부 로직 처리
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ public CloudResource registerResource(
ResourceRegistrationRequest request
) {
CloudProvider provider = findProvider(providerType);
CloudService service = findServiceOrNull(providerType, serviceKey);
CloudService service = findServiceOrCreate(providerType, serviceKey, provider);
Tenant tenant = findCurrentTenant();

CloudResource cloudResource = CloudResource.create(request, provider, service, tenant);
Expand Down Expand Up @@ -129,10 +129,28 @@ private CloudProvider findProvider(ProviderType providerType) {
"CloudProvider not found for type: " + providerType));
}

private CloudService findServiceOrNull(ProviderType providerType, String serviceKey) {
private CloudService findServiceOrCreate(ProviderType providerType, String serviceKey, CloudProvider provider) {
return cloudServiceRepository
.findByProviderTypeAndServiceKey(providerType, serviceKey)
.orElse(null);
.orElseGet(() -> {
log.info("[CloudResourceManagementHelper] CloudService not found, creating: providerType={}, serviceKey={}",
providerType, serviceKey);
CloudService newService = createDefaultService(providerType, serviceKey, provider);
return cloudServiceRepository.save(newService);
});
}

private CloudService createDefaultService(ProviderType providerType, String serviceKey, CloudProvider provider) {
return CloudService.builder()
.serviceKey(serviceKey)
.serviceName(serviceKey)
.displayName(null)
.provider(provider)
.status(com.agenticcp.core.common.enums.Status.ACTIVE)
.serviceType(null)
.serviceCategory(null)
.isRegionSpecific(true)
.build();
}

private Tenant findCurrentTenant() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,16 @@
import com.agenticcp.core.domain.user.dto.UpdateRoleRequest;
import com.agenticcp.core.domain.user.entity.Role;
import com.agenticcp.core.domain.user.service.RoleService;
import com.agenticcp.core.domain.user.service.UserService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;

import java.util.List;
Expand All @@ -32,6 +35,7 @@
public class RoleController {

private final RoleService roleService;
private final UserService userService;

@GetMapping
@Operation(summary = "모든 역할 조회", description = "현재 테넌트의 모든 역할을 조회합니다")
Expand Down Expand Up @@ -147,4 +151,47 @@ public ResponseEntity<ApiResponse<Void>> removePermissionFromRole(
roleService.removePermissionFromRole(roleId, permissionKey);
return ResponseEntity.ok(ApiResponse.success(null, "권한이 제거되었습니다"));
}

@PostMapping("/users/{username}")
@PreAuthorize("hasAuthority('USER_UPDATE') or hasRole('SUPER_ADMIN')")
@Operation(
summary = "사용자에게 역할 할당",
description = "사용자에게 역할을 할당합니다. 여러 역할을 동시에 할당할 수 있습니다."
)
@ApiResponses({
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "역할 할당 성공"),
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "400", description = "잘못된 요청 데이터 (역할을 찾을 수 없음 등)"),
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "403", description = "권한 없음 (USER_UPDATE 권한 또는 SUPER_ADMIN 역할 필요)"),
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "404", description = "사용자를 찾을 수 없음"),
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "500", description = "서버 내부 오류")
})
public ResponseEntity<ApiResponse<Void>> assignRolesToUser(
@Parameter(description = "사용자명", required = true, example = "testuser")
@PathVariable String username,
@Parameter(description = "역할 키 목록", required = true)
@RequestBody List<String> roleKeys) {
userService.assignRolesToUser(username, roleKeys);
return ResponseEntity.ok(ApiResponse.success(null, "역할이 할당되었습니다."));
}

@DeleteMapping("/users/{username}/{roleKey}")
@PreAuthorize("hasAuthority('USER_UPDATE') or hasRole('SUPER_ADMIN')")
@Operation(
summary = "사용자에서 역할 제거",
description = "사용자에서 특정 역할을 제거합니다."
)
@ApiResponses({
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "역할 제거 성공"),
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "403", description = "권한 없음"),
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "404", description = "사용자를 찾을 수 없음"),
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "500", description = "서버 내부 오류")
})
public ResponseEntity<ApiResponse<Void>> removeRoleFromUser(
@Parameter(description = "사용자명", required = true, example = "testuser")
@PathVariable String username,
@Parameter(description = "역할 키", required = true, example = "OBJECT_STORAGE_ADMIN")
@PathVariable String roleKey) {
userService.removeRoleFromUser(username, roleKey);
return ResponseEntity.ok(ApiResponse.success(null, "역할이 제거되었습니다."));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -216,4 +216,14 @@ public interface RoleRepository extends TenantAwareRepository<Role, Long> {
@Query("SELECT r FROM Role r WHERE r.tenant.tenantKey = :tenantKey AND r.isDeleted = false AND " +
"(r.roleKey LIKE %:keyword% OR r.roleName LIKE %:keyword% OR r.description LIKE %:keyword%)")
List<Role> searchRolesByTenantKey(@Param("keyword") String keyword, @Param("tenantKey") String tenantKey);

/**
* 역할 키 목록과 테넌트로 역할 목록 조회
*
* @param roleKeys 역할 키 목록
* @param tenant 테넌트
* @return 역할 목록
*/
@Query("SELECT r FROM Role r WHERE r.roleKey IN :roleKeys AND r.tenant = :tenant AND r.isDeleted = false")
List<Role> findByRoleKeyInAndTenant(@Param("roleKeys") List<String> roleKeys, @Param("tenant") Tenant tenant);
}
Original file line number Diff line number Diff line change
Expand Up @@ -298,8 +298,8 @@ public void assignPermissionsToRole(Long roleId, List<String> permissionKeys) {
Role role = roleRepository.findById(roleId)
.orElseThrow(() -> new ResourceNotFoundException(RoleErrorCode.ROLE_NOT_FOUND));

// 테넌트 확인
if (!role.getTenant().equals(currentTenant)) {
// 테넌트 확인 (ID 비교로 변경하여 lazy loading 문제 방지)
if (!role.getTenant().getId().equals(currentTenant.getId())) {
throw new BusinessException(RoleErrorCode.INVALID_TENANT_ACCESS);
}

Expand Down Expand Up @@ -346,16 +346,16 @@ public void removePermissionFromRole(Long roleId, String permissionKey) {
Role role = roleRepository.findById(roleId)
.orElseThrow(() -> new ResourceNotFoundException(RoleErrorCode.ROLE_NOT_FOUND));

// 테넌트 확인
if (!role.getTenant().equals(currentTenant)) {
// 테넌트 확인 (ID 비교로 변경하여 lazy loading 문제 방지)
if (!role.getTenant().getId().equals(currentTenant.getId())) {
throw new BusinessException(RoleErrorCode.INVALID_TENANT_ACCESS);
}

Permission permission = permissionRepository.findByPermissionKey(permissionKey)
.orElseThrow(() -> new ResourceNotFoundException(PermissionErrorCode.PERMISSION_NOT_FOUND));

// 테넌트 확인
if (!permission.getTenant().equals(currentTenant)) {
// 테넌트 확인 (ID 비교로 변경하여 lazy loading 문제 방지)
if (!permission.getTenant().getId().equals(currentTenant.getId())) {
throw new BusinessException(RoleErrorCode.INVALID_TENANT_PERMISSION_ACCESS);
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,14 @@
package com.agenticcp.core.domain.user.service;

import com.agenticcp.core.common.context.TenantContextHolder;
import com.agenticcp.core.common.exception.BusinessException;
import com.agenticcp.core.common.exception.ResourceNotFoundException;
import com.agenticcp.core.common.util.LogMaskingUtils;
import com.agenticcp.core.domain.user.enums.RoleErrorCode;
import com.agenticcp.core.domain.user.enums.UserErrorCode;
import com.agenticcp.core.domain.user.entity.Role;
import com.agenticcp.core.domain.user.entity.User;
import com.agenticcp.core.domain.user.repository.RoleRepository;
import com.agenticcp.core.domain.user.repository.UserRepository;
import com.agenticcp.core.common.enums.Status;
import com.agenticcp.core.common.enums.UserRole;
Expand Down Expand Up @@ -34,6 +40,7 @@
public class UserService {

private final UserRepository userRepository;
private final RoleRepository roleRepository;
private final PasswordEncoder passwordEncoder;
private final MaskingService maskingService;

Expand Down Expand Up @@ -316,4 +323,76 @@ public List<User> getUsersByStatus(Status status) {
log.info("[UserService] getUsersByStatus - found {} users", users.size());
return users;
}

/**
* 사용자에게 역할 할당
*
* @param username 사용자명
* @param roleKeys 역할 키 목록
*/
@Transactional
public void assignRolesToUser(String username, List<String> roleKeys) {
Tenant currentTenant = TenantContextHolder.getCurrentTenantOrThrow();
log.info("[UserService] assignRolesToUser - username={} roleKeys={} tenantKey={}",
LogMaskingUtils.mask(username, 2, 2),
roleKeys == null ? 0 : roleKeys.size(),
LogMaskingUtils.maskTenantKey(currentTenant.getTenantKey()));

User user = getUserByUsernameOrThrow(username);

// 테넌트 확인
if (!user.getTenant().getId().equals(currentTenant.getId())) {
throw new BusinessException(UserErrorCode.USER_NOT_FOUND, "다른 테넌트의 사용자입니다.");
}

// 역할 조회 및 테넌트 확인
List<Role> roles = roleRepository.findByRoleKeyInAndTenant(roleKeys, currentTenant);

if (roles.size() != roleKeys.size()) {
throw new BusinessException(RoleErrorCode.ROLE_NOT_FOUND, "일부 역할을 찾을 수 없습니다.");
}

// 사용자에게 역할 할당
user.setRoles(roles);
userRepository.save(user);

log.info("[UserService] assignRolesToUser - success username={} tenantKey={}",
LogMaskingUtils.mask(username, 2, 2),
LogMaskingUtils.maskTenantKey(currentTenant.getTenantKey()));
}

/**
* 사용자에서 역할 제거
*
* @param username 사용자명
* @param roleKey 역할 키
*/
@Transactional
public void removeRoleFromUser(String username, String roleKey) {
Tenant currentTenant = TenantContextHolder.getCurrentTenantOrThrow();
log.info("[UserService] removeRoleFromUser - username={} roleKey={} tenantKey={}",
LogMaskingUtils.mask(username, 2, 2),
LogMaskingUtils.mask(roleKey, 2, 2),
LogMaskingUtils.maskTenantKey(currentTenant.getTenantKey()));

User user = getUserByUsernameOrThrow(username);

// 테넌트 확인
if (!user.getTenant().getId().equals(currentTenant.getId())) {
throw new BusinessException(UserErrorCode.USER_NOT_FOUND, "다른 테넌트의 사용자입니다.");
}

// 역할 제거
List<Role> roles = user.getRoles();
if (roles != null) {
roles.removeIf(role -> role.getRoleKey().equals(roleKey));
user.setRoles(roles);
userRepository.save(user);
}

log.info("[UserService] removeRoleFromUser - success username={} roleKey={} tenantKey={}",
LogMaskingUtils.mask(username, 2, 2),
LogMaskingUtils.mask(roleKey, 2, 2),
LogMaskingUtils.maskTenantKey(currentTenant.getTenantKey()));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ void setUp() {
.tenantKey("tenant-001")
.tenantName("테넌트001")
.build();
tenant.setId(1L);
TenantContextHolder.setTenant(tenant);
}

Expand Down