@restrict 한 줄로 끝 — CAP Node.js row-level 보안 실전 #shorts #SAP #CAPforNode

Moderator · 조회 2
슬라이드1 슬라이드2 슬라이드3

1. 도입 — 왜 Authorization을 처음부터 설계해야 하는가

CAP for Node.js로 서비스를 빠르게 만들다 보면 "일단 동작부터" 만들고 인증/인가는 나중에 붙이려는 유혹이 큽니다. 하지만 SAP의 Secure by Default 원칙은 처음부터 보안을 설계하지 않으면 나중에 비즈니스 규칙이 누더기처럼 코드 곳곳에 흩어진다고 경고합니다. CAP는 다행히 @requires@restrict라는 두 어노테이션만으로 서비스/엔티티/액션 단위의 접근 제어를 CDS 모델에 선언적으로 표현할 수 있게 해줍니다. 즉, "이 서비스는 누가 호출할 수 있고, 이 엔티티의 어떤 인스턴스에 어떤 동작을 허용할 것인가"를 코드가 아닌 모델 레벨에서 한 줄로 끝낼 수 있다는 뜻입니다.

학습 체크리스트

2. CAP 인증 기본 이해

CAP에서 인증의 결과는 cds.context.user 객체에 담깁니다. 이 객체는 다음 정보를 제공하는 일반적인 추상화입니다.

인증 전략은 package.jsoncds.requires.auth로 선택합니다. 로컬에서는 mocked, 클라우드에서는 xsuaaias를 권장합니다.

{
  "cds": {
    "requires": {
      "auth": {
        "kind": "mocked",
        "users": {
          "alice": { "roles": ["SalesManager"] },
          "bob":   { "roles": ["SalesRep"], "attr": { "regions": "EU" } }
        }
      }
    }
  }
}

특수 사용자도 알아두면 유용합니다. cds.User.Privileged는 내부 작업에서 권한 검사를 우회하고, cds.User.Anonymous는 미인증 사용자를 표현합니다. 두 값 모두 명시적으로 사용해야 하며, 일반 핸들러에서 권한을 무력화하는 용도로 남용하면 안 됩니다.

3. @requires 어노테이션 실전

@requires는 "이 객체에 접근하려면 누구여야 하는가"를 선언합니다. 가장 단순한 형태는 인증된 사용자만 허용하는 것입니다.

// 서비스 전체를 인증된 사용자에게만
annotate BrowseBooksService with @(requires: 'authenticated-user');

// 특정 역할 OR 조건 — Vendor 또는 ProcurementManager
annotate ShopService.Books with @(requires: ['Vendor', 'ProcurementManager']);

// 액션/함수에도 적용 가능
annotate ShopService.submitOrder with @(requires: 'Customer');

여러 역할을 배열로 나열하면 OR 로직이 적용됩니다(둘 중 하나만 보유해도 통과). 적용 대상은 서비스/엔티티/액션/함수 모두이며, 계층적으로 평가됩니다. 예컨대 서비스에 authenticated-user가 있고 엔티티에 Vendor가 있다면, 두 조건을 모두 만족해야 접근이 허용됩니다.

실무에서는 "기본은 인증된 사용자, 민감 엔티티만 추가 역할"이라는 패턴이 일반적으로 권장됩니다.

4. @restrict 어노테이션 심화

@requires가 통과 조건을 단순히 선언한다면, @restrict는 "어떤 이벤트를, 어떤 역할에게, 어떤 조건에서" 허용할지 세밀하게 표현합니다. 기본 구조는 { grant, to, where } 트리플입니다.

service SalesOrderService @(requires: 'authenticated-user') {

  entity SalesOrders @(restrict: [
    // SalesRep은 자신 지역의 주문만 읽기/쓰기
    { grant: ['READ', 'WRITE'],
      to: 'SalesRep',
      where: '$user.regions = region' },

    // SalesManager는 전체 권한 (액션 포함 *)
    { grant: '*',
      to: 'SalesManager' },

    // 모든 인증 사용자는 READ 가능, 단 공개된 주문만
    { grant: 'READ',
      where: 'isPublic = true' }
  ]) {
    key ID    : UUID;
    region    : String;
    isPublic  : Boolean;
    total     : Decimal;
  }

  action approveOrder(ID: UUID) returns String;
  annotate approveOrder with @(restrict: [
    { grant: 'EXECUTE', to: 'SalesManager' }
  ]);
}

각 키의 의미는 다음과 같습니다.

여러 항목이 배열로 들어가면 OR로 결합됩니다. 즉, 조건 중 하나라도 만족하면 허용됩니다.

5. 인스턴스 수준 보안 (where 절)

진짜 강력한 부분은 where입니다. CAP는 이 조건을 이벤트 종류에 따라 다르게 적용합니다.

가장 자주 쓰는 패턴은 exists 서브쿼리로 멤버십을 검사하는 것입니다.

entity Projects @(restrict: [
  { grant: ['READ', 'WRITE'],
    where: 'exists members[userId = $user and role = ''Editor'']' }
]) {
  key ID   : UUID;
  title    : String;
  members  : Composition of many ProjectMembers on members.project = $self;
}

entity ProjectMembers {
  key project : Association to Projects;
  key userId  : String;
  role        : String; // 'Editor' | 'Viewer'
}

위 모델에서 사용자는 자신이 Editor로 등록된 프로젝트에만 접근할 수 있습니다. 핸들러 코드를 단 한 줄도 작성하지 않고도 row-level security가 동작합니다. 사용자 속성을 활용하는 방식도 자주 쓰입니다.

entity Invoices @(restrict: [
  { grant: 'READ', to: 'Accountant',
    where: '$user.costCenters = costCenter' },
  { grant: '*', to: 'CFO' }
]) {
  key ID      : UUID;
  costCenter  : String;
  amount      : Decimal;
}

$user.costCenters는 토큰의 xs.user.attributes.costCenters에서 채워지며, XSUAA의 xs-security.json에 attribute 정의가 있어야 합니다.

6. XSUAA 연동 실전

로컬 mocked 인증으로 모델을 검증한 뒤에는 BTP에서 XSUAA로 hybrid 테스트하는 것이 일반적인 흐름입니다.

# 1) XSUAA 인스턴스 생성
cf create-service xsuaa application bookshop-uaa -c xs-security.json

# 2) CAP 프로젝트에 바인딩 (.cdsrc-private.json 자동 생성)
cds bind -2 bookshop-uaa

# 3) 하이브리드 모드로 실행
cds watch --profile hybrid

xs-security.json에 역할 컬렉션과 attribute를 정의합니다.

{
  "xsappname": "bookshop",
  "tenant-mode": "dedicated",
  "scopes": [
    { "name": "$XSAPPNAME.SalesRep" },
    { "name": "$XSAPPNAME.SalesManager" }
  ],
  "attributes": [
    { "name": "regions", "valueType": "string" }
  ],
  "role-templates": [
    { "name": "SalesRep", "scope-references": ["$XSAPPNAME.SalesRep"],
      "attribute-references": ["regions"] },
    { "name": "SalesManager", "scope-references": ["$XSAPPNAME.SalesManager"] }
  ]
}

주의할 성능 포인트가 있습니다. @sap/xssec 4.8 이상은 signature cache와 token decode cache를 내장하므로, 가능하면 최신 마이너 버전을 사용하는 것이 권장됩니다. 또한 바인딩 정보를 수동으로 복사해 코드에 넣는 행위는 절대 금지입니다(시크릿 유출 위험). 항상 cds bind 또는 VCAP_SERVICES를 통해 주입받으세요.

7. 보안 설계 Best Practices

8. 흔한 실수 / 트러블슈팅 FAQ

Q1. 로컬에서는 되는데 BTP에서 401이 떨어집니다.
A. cds.requires.auth.kindmocked로 고정돼 있는지 확인하세요. --profile hybrid 또는 production 프로파일에서 xsuaa로 전환되어야 합니다.

Q2. $user.regions = region이 항상 false로 평가됩니다.
A. XSUAA attribute 이름이 xs-security.json에 정의돼 있고, 역할 컬렉션 할당 시 attribute 값이 채워졌는지 BTP 콕핏에서 확인하세요. attribute가 없으면 비교는 실패합니다.

Q3. 내부 배치 작업에서 권한 오류가 납니다.
A. cds.tx({ user: cds.User.Privileged }, async tx => { ... })로 명시적 권한 우회 컨텍스트를 만드세요. 단, 이 패턴은 신뢰 가능한 시스템 작업에만 한정합니다.

9. 다음 단계 / 관련 주제

Authorization을 익혔다면 다음 단계로 Multitenancy(@sap/cds-mtxs)Audit Logging을 학습하길 권장합니다. 멀티테넌시는 $user에 추가로 tenant 격리를 강제하므로 설계가 한층 복잡해집니다. 또한 IAS 기반 Principal Propagation, Approuter와의 연계, 토큰 교환(token exchange)도 실무에서 자주 등장합니다.

10. 참고 자료

핵심 한 줄

CAP의 보안은 코드가 아니라 모델이다 — @requires로 문을 잠그고, @restrictwhere로 방을 나누면, 핸들러 한 줄 없이 row-level security까지 끝난다.