CAP for Java

@AuthorizedRole 빠뜨리면 큰일 — CAP Java 권한 설정 #shorts #SAP #CAP

▶ YouTube에서 보기

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

아직 댓글이 없습니다.