package com.example.hrlab.security; import com.example.hrlab.audit.AuditService; import com.example.hrlab.common.ApiResponse; import com.example.hrlab.common.BusinessException; import com.example.hrlab.common.ErrorCodes; import com.example.hrlab.common.Rows; import jakarta.validation.Valid; import jakarta.validation.constraints.NotBlank; import java.util.List; import java.util.Map; import org.springframework.http.HttpStatus; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.transaction.annotation.Transactional; import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.PutMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @RestController @RequestMapping("/api/v1/security") public class SecurityAdminController { private final JdbcTemplate jdbcTemplate; private final AuditService auditService; public SecurityAdminController(JdbcTemplate jdbcTemplate, AuditService auditService) { this.jdbcTemplate = jdbcTemplate; this.auditService = auditService; } @GetMapping("/roles") @PreAuthorize("hasRole('SYSTEM_ADMIN')") public ApiResponse roles() { return ApiResponse.ok(jdbcTemplate.queryForList(""" SELECT *, role_code IN ('SYSTEM_ADMIN','HR_ADMIN','RECRUITER','DEPT_MANAGER','PAYROLL_ADMIN', 'EMPLOYEE_SELF','AUDITOR','INTEGRATION_CLIENT') AS builtin FROM roles ORDER BY role_code """)); } @GetMapping("/permissions") @PreAuthorize("hasRole('SYSTEM_ADMIN')") public ApiResponse permissions() { return ApiResponse.ok(jdbcTemplate.queryForList(""" SELECT *, permission_code IN ('system:admin','employee:write','employee:read','recruitment:write', 'attendance:write','payroll:write','performance:write','report:export','audit:read','integration:access') AS builtin FROM permissions ORDER BY permission_code """)); } @PostMapping("/roles") @PreAuthorize("hasRole('SYSTEM_ADMIN')") public ApiResponse> createRole(@Valid @RequestBody RoleRequest request) { Long id = jdbcTemplate.queryForObject(""" INSERT INTO roles(role_code, role_name, data_scope) VALUES (?, ?, ?) RETURNING id """, Long.class, request.roleCode(), request.roleName(), request.dataScope()); auditService.record("ROLE_CREATE", "roles", id, null, request); return ApiResponse.ok(findRole(id)); } @PutMapping("/roles/{id}") @PreAuthorize("hasRole('SYSTEM_ADMIN')") public ApiResponse> updateRole(@PathVariable Long id, @Valid @RequestBody RoleRequest request) { Map before = findRole(id); SecurityCatalogPolicy.requireRoleCodeMutable(String.valueOf(before.get("role_code")), request.roleCode()); jdbcTemplate.update("UPDATE roles SET role_code = ?, role_name = ?, data_scope = ?, updated_at = now() WHERE id = ?", request.roleCode(), request.roleName(), request.dataScope(), id); auditService.record("ROLE_UPDATE", "roles", id, before, request); return ApiResponse.ok(findRole(id)); } @DeleteMapping("/roles/{id}") @PreAuthorize("hasRole('SYSTEM_ADMIN')") public ApiResponse> deleteRole(@PathVariable Long id) { Map before = findRole(id); String roleCode = String.valueOf(before.get("role_code")); SecurityCatalogPolicy.requireRoleDeletable(roleCode); Long boundUsers = jdbcTemplate.queryForObject("SELECT count(*) FROM user_roles WHERE role_id = ?", Long.class, id); if (boundUsers != null && boundUsers > 0) { throw new BusinessException(ErrorCodes.BUSINESS_RULE_VIOLATION, "角色已分配给用户,不能删除"); } jdbcTemplate.update("DELETE FROM roles WHERE id = ?", id); auditService.record("ROLE_DELETE", "roles", id, before, Map.of("deleted", true)); return ApiResponse.ok(Map.of("deleted", true)); } @GetMapping("/roles/{id}/permissions") @PreAuthorize("hasRole('SYSTEM_ADMIN')") public ApiResponse rolePermissions(@PathVariable Long id) { findRole(id); return ApiResponse.ok(permissionsForRole(id)); } @PutMapping("/roles/{id}/permissions") @PreAuthorize("hasRole('SYSTEM_ADMIN')") @Transactional public ApiResponse> replaceRolePermissions(@PathVariable Long id, @RequestBody PermissionCodesRequest request) { findRole(id); Map before = Map.of("permissions", permissionsForRole(id)); jdbcTemplate.update("DELETE FROM role_permissions WHERE role_id = ?", id); for (String code : request.permissionCodes()) { jdbcTemplate.update(""" INSERT INTO role_permissions(role_id, permission_id) SELECT ?, id FROM permissions WHERE permission_code = ? """, id, code); } Map after = Map.of("permissions", permissionsForRole(id)); auditService.record("ROLE_PERMISSION_REPLACE", "roles", id, before, after); return ApiResponse.ok(after); } @PostMapping("/permissions") @PreAuthorize("hasRole('SYSTEM_ADMIN')") public ApiResponse> createPermission(@Valid @RequestBody PermissionRequest request) { Long id = jdbcTemplate.queryForObject(""" INSERT INTO permissions(permission_code, permission_name) VALUES (?, ?) RETURNING id """, Long.class, request.permissionCode(), request.permissionName()); auditService.record("PERMISSION_CREATE", "permissions", id, null, request); return ApiResponse.ok(findPermission(id)); } @PutMapping("/permissions/{id}") @PreAuthorize("hasRole('SYSTEM_ADMIN')") public ApiResponse> updatePermission(@PathVariable Long id, @Valid @RequestBody PermissionRequest request) { Map before = findPermission(id); SecurityCatalogPolicy.requirePermissionCodeMutable(String.valueOf(before.get("permission_code")), request.permissionCode()); jdbcTemplate.update("UPDATE permissions SET permission_code = ?, permission_name = ?, updated_at = now() WHERE id = ?", request.permissionCode(), request.permissionName(), id); auditService.record("PERMISSION_UPDATE", "permissions", id, before, request); return ApiResponse.ok(findPermission(id)); } @DeleteMapping("/permissions/{id}") @PreAuthorize("hasRole('SYSTEM_ADMIN')") @Transactional public ApiResponse> deletePermission(@PathVariable Long id) { Map before = findPermission(id); SecurityCatalogPolicy.requirePermissionDeletable(String.valueOf(before.get("permission_code"))); jdbcTemplate.update("DELETE FROM role_permissions WHERE permission_id = ?", id); jdbcTemplate.update("DELETE FROM permissions WHERE id = ?", id); auditService.record("PERMISSION_DELETE", "permissions", id, before, Map.of("deleted", true)); return ApiResponse.ok(Map.of("deleted", true)); } private List permissionsForRole(Long roleId) { return jdbcTemplate.queryForList(""" SELECT p.permission_code FROM permissions p JOIN role_permissions rp ON rp.permission_id = p.id WHERE rp.role_id = ? ORDER BY p.permission_code """, String.class, roleId); } private Map findRole(Long id) { return jdbcTemplate.query("SELECT * FROM roles WHERE id = ?", rs -> { if (!rs.next()) { throw new BusinessException(ErrorCodes.NOT_FOUND, "role not found", HttpStatus.NOT_FOUND); } return Rows.map(rs); }, id); } private Map findPermission(Long id) { return jdbcTemplate.query("SELECT * FROM permissions WHERE id = ?", rs -> { if (!rs.next()) { throw new BusinessException(ErrorCodes.NOT_FOUND, "权限不存在", HttpStatus.NOT_FOUND); } return Rows.map(rs); }, id); } public record RoleRequest(@NotBlank String roleCode, @NotBlank String roleName, @NotBlank String dataScope) { } public record PermissionRequest(@NotBlank String permissionCode, @NotBlank String permissionName) { } public record PermissionCodesRequest(List permissionCodes) { } }