CAP Java에서 권한 설정을 빠뜨리면 생기는 일
CAP for Java 애플리케이션에서 권한 설정을 하지 않으면 인증된 모든 사용자가 모든 데이터에 접근할 수 있습니다. BTP XSUAA와 연동된 환경에서는 기본적으로 인증(Authentication)은 요구하지만, 권한(Authorization)은 개발자가 명시적으로 설정해야 합니다. @RequiresRole, @Restrict 어노테이션을 올바르게 사용하는 방법을 다룹니다.
권한 없는 기본 서비스 — 모든 사용자가 접근 가능
// 위험한 패턴: 권한 설정 없음
@Component
@ServiceName("PayrollService")
public class PayrollServiceHandler implements EventHandler {
@On(event = CqnService.EVENT_READ, entity = SalarySlips_.CDS_NAME)
public void readSalaries(CdsReadEventContext ctx) {
// 인증만 되면 누구든 급여 데이터를 볼 수 있음
// 일반 직원이 임원 급여를 볼 수 있는 상황
var result = persistenceService.run(ctx.getCqn());
ctx.setResult(result);
}
}
CDS 레벨 권한 설정 — @requires와 @restrict
// service.cds
service PayrollService @(requires: 'authenticated-user') {
// 급여 담당자만 읽기 가능
@(restrict: [
{ grant: 'READ', to: 'PayrollAdmin' },
{ grant: 'READ', to: 'HRManager' }
])
entity SalarySlips as projection on db.SalarySlips;
// 인사 담당자만 CRUD
@(restrict: [
{ grant: ['READ', 'CREATE', 'UPDATE'], to: 'HRManager' },
{ grant: 'READ', to: 'Employee', where: 'employee_id = $user' }
])
entity EmployeeProfiles as projection on db.EmployeeProfiles;
// 관리자만 삭제 가능
@(restrict: [
{ grant: '*', to: 'PayrollAdmin' },
{ grant: ['READ', 'CREATE', 'UPDATE'], to: 'HRManager' }
])
entity PayrollPeriods as projection on db.PayrollPeriods;
}
where: 'employee_id = $user' 조건은 현재 로그인한 사용자가 자신의 데이터만 볼 수 있도록 행 수준 필터를 적용합니다.
XSUAA xs-security.json에 Role 정의
{
"xsappname": "payroll-app",
"tenant-mode": "dedicated",
"scopes": [
{ "name": "$XSAPPNAME.PayrollAdmin", "description": "급여 전체 관리" },
{ "name": "$XSAPPNAME.HRManager", "description": "인사 관리" },
{ "name": "$XSAPPNAME.Employee", "description": "일반 직원" }
],
"role-templates": [
{
"name": "PayrollAdmin",
"scope-references": ["$XSAPPNAME.PayrollAdmin"]
},
{
"name": "HRManager",
"scope-references": [
"$XSAPPNAME.HRManager",
"$XSAPPNAME.Employee"
]
},
{
"name": "Employee",
"scope-references": ["$XSAPPNAME.Employee"]
}
]
}
Java 핸들러에서 수동 권한 체크
// CDS @restrict 외에 Java 코드에서 추가 권한 체크
@Component
@ServiceName("PayrollService")
public class PayrollServiceHandler implements EventHandler {
@Before(event = CqnService.EVENT_CREATE, entity = SalarySlips_.CDS_NAME)
public void beforeCreateSalary(CdsCreateEventContext ctx) {
// 현재 사용자 정보 확인
UserInfo userInfo = ctx.getUserInfo();
// PayrollAdmin 역할 체크
if (!userInfo.hasRole("PayrollAdmin")) {
throw new ServiceException(ErrorStatuses.FORBIDDEN,
"급여 데이터 생성은 PayrollAdmin만 가능합니다.");
}
// 추가 비즈니스 규칙: 현재 급여 기간만 생성 가능
var data = ctx.getCqn().entries().get(0);
String period = data.get("payroll_period").toString();
if (!isCurrentPeriod(period)) {
throw new ServiceException(ErrorStatuses.BAD_REQUEST,
"현재 급여 기간(" + getCurrentPeriod() + ")만 생성할 수 있습니다.");
}
}
@On(event = CqnService.EVENT_READ, entity = SalarySlips_.CDS_NAME)
public void readSalaries(CdsReadEventContext ctx) {
UserInfo userInfo = ctx.getUserInfo();
// 일반 직원은 자신의 급여만 조회
if (userInfo.hasRole("Employee") && !userInfo.hasRole("HRManager")) {
String userId = userInfo.getName();
// CQL에 필터 추가
var originalQuery = ctx.getCqn();
var filteredQuery = Select.copy(originalQuery)
.where(s -> s.get("employee_id").eq(userId));
ctx.setCqn(filteredQuery);
}
var result = persistenceService.run(ctx.getCqn());
ctx.setResult(result);
}
}
테스트에서 역할 시뮬레이션
@SpringBootTest
@TestPropertySource(properties = {
"cds.security.authentication.mode=mock"
})
class PayrollServiceTest {
@Autowired
private PayrollService payrollService;
@Test
@WithMockUser(roles = {"PayrollAdmin"})
void testAdminCanReadAllSalaries() {
var result = payrollService.run(Select.from(SalarySlips_.class));
assertFalse(result.list().isEmpty());
}
@Test
@WithMockUser(username = "emp001", roles = {"Employee"})
void testEmployeeCanOnlySeeOwnSalary() {
var result = payrollService.run(Select.from(SalarySlips_.class));
result.forEach(slip -> {
assertEquals("emp001", slip.get("employee_id"));
});
}
@Test
@WithMockUser(roles = {"Employee"})
void testEmployeeCannotCreateSalary() {
assertThrows(ServiceException.class, () -> {
payrollService.run(Insert.into(SalarySlips_.class)
.entry(Map.of("employee_id", "emp002", "amount", 5000000)));
});
}
}
체크리스트
- 모든 엔티티에 @restrict 또는 @requires 설정 여부 확인
- xs-security.json의 scopes와 CDS의 역할 이름 일치 확인
- 행 수준 필터(where: 'field = $user')가 필요한 엔티티 식별
- 단위 테스트에서 역할별 접근 시나리오 검증
공식 문서
CAP Java 인증/권한 가이드는 cap.cloud.sap/docs/java/security에서 확인하세요.
댓글 0
아직 댓글이 없습니다.