Chuyển đến nội dung chính

レッスン 6: RBAC と ABAC の分散化 — 医師、看護師、患者

医療向けにロールベースのアクセス制御 (RBAC) と属性ベースのアクセス制御 (ABAC) を導入します。ロール階層 (部門の医師、治療医、主任看護師、看護師、技術者、管理者、患者)、部門ベースのアクセス、ポリシーと権限を備えた Keycloak 認可サービス、およびガラス越しの緊急アクセスを設計します。

🏗️ アーキテクチャ — レッスン 6 レッスン 6: RBAC と ABAC の分散化 — 医師、看護師 看護師、患者

マイクロサービス ヘルスケア システムの構築 — HIPAA 標準を備えた Quarkus、PostgreSQL、Keycloak

パート 2: Keycloak を使用した ID とアクセス管理

xdev.asia

1. ヘルスケアにおける RBAC と ABAC

4 層のアクセス制御アーキテクチャ: RBAC、ABAC、RLS、患者の同意

###1.1.役割ベースのアクセス制御 (RBAC)

RBAC は、組織内のユーザーの役割に基づいて権限を割り当てます。

User ──▶ Role ──▶ Permission

Ví dụ:
BS. Nguyễn ──▶ doctor ──▶ patient_read, patient_write, prescription_write
ĐD. Trần   ──▶ nurse  ──▶ patient_read, vital_signs_write
BN. Lê     ──▶ patient ──▶ own_record_read

利点: シンプル、管理、監査が簡単 制限事項: 複雑なルールには対応できません (例: 内科の患者は内科の医師のみが診察可能)

###1.2.属性ベースのアクセス制御 (ABAC)

ABAC は、ユーザー、リソース、アクション、環境の 属性 に基づいて決定を行います。

Policy: ALLOW if
  user.role == "doctor" AND
  user.department == resource.department AND
  resource.classification_level <= 3 AND
  environment.time BETWEEN "06:00" AND "22:00" AND
  environment.network == "INTERNAL"

利点: 柔軟、きめ細かく、コンテキストを認識できる 制限事項: より複雑でデバッグが難しく、多くのメタデータが必要です

###1.3. RBAC + ABAC を組み合わせて健康を実現

レイヤー名前メカニズム例
レイヤー 1RBAC (Keycloak ロール)ユーザーは医師ですか? → 臨床データにアクセスできる。ユーザーは看護師ですか? → バイタルサインにアクセスできる。ユーザーは患者ですか? → 自分の記録にアクセスできる。@RolesAllowed("doctor")
レイヤー 2ABAC (Keycloak認可サービス)学科対抗? →自部門データのみ。治療関係? → 割り当てられた患者のみ。時間ベースですか? → 時間外にはMFAが必要です。位置情報ベースですか? → 外部にはVPNが必要です。カスタムポリシー
レイヤー 3RLS (PostgreSQL 行レベル セキュリティ)データベースは JWT クレームに基づいて行ごとのアクセスを強制します。 CREATE POLICY
レイヤー 4同意(患者同意確認)患者はこの目的でアクセスを許可しましたか?FHIR 同意リソース

2. Keycloak認可サービス

###2.1.コンセプト

Resources      → What is being protected? (Patient Record, Lab Result)
Scopes         → What actions? (read, write, delete, export, print)
Policies       → Who can access? (role, user, time, script)
Permissions    → Connect Resources + Scopes + Policies

###2.2.リソースの定義

{
  "resources": [
    {
      "name": "Patient Record",
      "type": "urn:healthcare:resources:patient",
      "uris": ["/api/v1/patients/*"],
      "ownerManagedAccess": false,
      "scopes": [
        {"name": "read"},
        {"name": "write"},
        {"name": "delete"},
        {"name": "export"},
        {"name": "print"}
      ],
      "attributes": {
        "classification": ["3"],
        "contains_phi": ["true"]
      }
    },
    {
      "name": "Lab Result",
      "type": "urn:healthcare:resources:lab-result",
      "uris": ["/api/v1/lab-results/*"],
      "scopes": [
        {"name": "read"},
        {"name": "write"},
        {"name": "verify"},
        {"name": "print"}
      ]
    },
    {
      "name": "Prescription",
      "type": "urn:healthcare:resources:prescription",
      "uris": ["/api/v1/prescriptions/*"],
      "scopes": [
        {"name": "read"},
        {"name": "write"},
        {"name": "dispense"},
        {"name": "cancel"}
      ]
    }
  ]
}

###2.3.ポリシーの定義

{
  "policies": [
    {
      "name": "Doctor Role Policy",
      "type": "role",
      "logic": "POSITIVE",
      "config": {
        "roles": [
          {"id": "doctor", "required": true}
        ]
      }
    },
    {
      "name": "Same Department Policy",
      "type": "script-js",
      "logic": "POSITIVE",
      "config": {
        "code": "var context = $evaluation.getContext(); var identity = context.getIdentity(); var userDept = identity.getAttributes().getValue('department').asString(0); var resourceDept = $evaluation.getPermission().getResource().getAttribute('department')[0]; if (userDept === resourceDept) { $evaluation.grant(); }"
      }
    },
    {
      "name": "Working Hours Policy",
      "type": "time",
      "logic": "POSITIVE",
      "config": {
        "notBefore": "06:00",
        "notOnOrAfter": "22:00"
      }
    },
    {
      "name": "Emergency Access Policy",
      "type": "role",
      "logic": "POSITIVE",
      "config": {
        "roles": [
          {"id": "emergency_access", "required": true}
        ]
      }
    }
  ]
}

###2.4.権限

{
  "permissions": [
    {
      "name": "Doctor Read Patient",
      "type": "scope",
      "resources": ["Patient Record"],
      "scopes": ["read"],
      "policies": ["Doctor Role Policy", "Same Department Policy"],
      "decisionStrategy": "AFFIRMATIVE"
    },
    {
      "name": "Doctor Write Prescription",
      "type": "scope",
      "resources": ["Prescription"],
      "scopes": ["write"],
      "policies": ["Doctor Role Policy"],
      "decisionStrategy": "UNANIMOUS"
    },
    {
      "name": "Emergency Override",
      "type": "scope",
      "resources": ["Patient Record", "Lab Result", "Prescription"],
      "scopes": ["read"],
      "policies": ["Emergency Access Policy"],
      "decisionStrategy": "AFFIRMATIVE"
    }
  ]
}

3. Quarkus での実装

###3.1. @RolesAllowed を使用した RBAC

@Path("/api/v1/patients")
@Authenticated
public class PatientResource {

    @GET
    @RolesAllowed({"doctor", "nurse", "admin"})
    public List<PatientSummaryDTO> listPatients(
            @QueryParam("department") String department) {
        // Role check passed — now apply ABAC
        return patientService.findByDepartment(department);
    }

    @GET
    @Path("/{id}")
    @RolesAllowed({"doctor", "nurse", "patient"})
    public PatientDetailDTO getPatient(@PathParam("id") UUID id) {
        return patientService.findById(id);
    }

    @PUT
    @Path("/{id}")
    @RolesAllowed({"doctor", "admin"})
    public Response updatePatient(
            @PathParam("id") UUID id,
            PatientUpdateRequest request) {
        return patientService.update(id, request);
    }

    @DELETE
    @Path("/{id}")
    @RolesAllowed({"admin"})
    public Response deletePatient(@PathParam("id") UUID id) {
        // Soft delete only — PHI retention requirements
        return patientService.softDelete(id);
    }
}

###3.2. ABAC とカスタム SecurityIdentityAugmentor

@ApplicationScoped
public class HealthcareSecurityAugmentor implements SecurityIdentityAugmentor {

    @Inject
    TreatmentRelationshipService treatmentService;

    @Override
    public Uni<SecurityIdentity> augment(SecurityIdentity identity,
            AuthenticationRequestContext context) {

        return Uni.createFrom().item(() -> {
            QuarkusSecurityIdentity.Builder builder =
                QuarkusSecurityIdentity.builder(identity);

            // Add department attribute from JWT
            String department = identity.getAttribute("department");
            if (department != null) {
                builder.addAttribute("department", department);
            }

            // Add hospital code
            String hospitalCode = identity.getAttribute("hospitalCode");
            if (hospitalCode != null) {
                builder.addAttribute("hospitalCode", hospitalCode);
            }

            return builder.build();
        });
    }
}

###3.3. ABAC認可インターセプター

@Interceptor
@HealthcareAuthorization
@Priority(Interceptor.Priority.APPLICATION)
public class HealthcareAuthorizationInterceptor {

    @Inject
    SecurityIdentity identity;

    @Inject
    AuditService auditService;

    @AroundInvoke
    public Object authorize(InvocationContext ctx) throws Exception {
        HealthcareAuthorization annotation =
            ctx.getMethod().getAnnotation(HealthcareAuthorization.class);

        if (annotation == null) {
            return ctx.proceed();
        }

        // Check department access
        if (annotation.requireSameDepartment()) {
            String userDept = identity.getAttribute("department");
            UUID resourceId = extractResourceId(ctx);
            String resourceDept = getResourceDepartment(resourceId);

            if (!userDept.equals(resourceDept) &&
                !identity.hasRole("emergency_access")) {
                auditService.logAccessDenied(identity,
                    annotation.resource(), resourceId.toString(),
                    "Department mismatch: user=" + userDept +
                    " resource=" + resourceDept);
                throw new ForbiddenException(
                    "Access denied: department mismatch");
            }
        }

        // Check treatment relationship
        if (annotation.requireTreatmentRelationship()) {
            UUID patientId = extractPatientId(ctx);
            String doctorId = identity.getPrincipal().getName();

            if (!treatmentService.hasRelationship(doctorId, patientId)) {
                auditService.logAccessDenied(identity,
                    annotation.resource(), patientId.toString(),
                    "No treatment relationship");
                throw new ForbiddenException(
                    "No treatment relationship with patient");
            }
        }

        return ctx.proceed();
    }
}

###3.4.カスタム認可アノテーション

@InterceptorBinding
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD, ElementType.TYPE})
public @interface HealthcareAuthorization {
    String resource() default "";
    boolean requireSameDepartment() default false;
    boolean requireTreatmentRelationship() default false;
    int maxClassificationLevel() default 3;
}

// Usage
@GET
@Path("/{patientId}/clinical-notes")
@RolesAllowed("doctor")
@HealthcareAuthorization(
    resource = "ClinicalNote",
    requireSameDepartment = true,
    requireTreatmentRelationship = true
)
public List<ClinicalNoteDTO> getClinicalNotes(
        @PathParam("patientId") UUID patientId) {
    return clinicalNoteService.findByPatient(patientId);
}

4. ガラスを割った時の緊急アクセス

###4.1.コンセプト

Tình huống: Bệnh nhân cấp cứu, bác sĩ trực KHÔNG phải bác sĩ
điều trị → cần truy cập hồ sơ bệnh án NGAY LẬP TỨC

Normal Flow:        Emergency Flow:
Doctor ──▶ Check    Doctor ──▶ Break-the-Glass
           Role ✓              ▶ Log reason
           Dept ✓              ▶ Grant temporary access
           Rel. ✓              ▶ Alert security team
           ──▶ Access          ▶ Time-limited (2 hours)
                               ──▶ Access
                               ──▶ Post-review required

###4.2.実装

@Path("/api/v1/emergency-access")
@Authenticated
public class EmergencyAccessResource {

    @Inject
    SecurityIdentity identity;

    @Inject
    EmergencyAccessService emergencyService;

    @Inject
    NotificationService notificationService;

    @POST
    @RolesAllowed({"doctor", "nurse"})
    public Response requestEmergencyAccess(EmergencyAccessRequest request) {

        // Validate and log emergency access
        EmergencyAccess access = emergencyService.create(
            EmergencyAccess.builder()
                .requesterId(identity.getPrincipal().getName())
                .requesterRole(identity.getRoles().toString())
                .patientId(request.patientId())
                .reason(request.reason())  // Required: why emergency access?
                .expiresAt(Instant.now().plus(2, ChronoUnit.HOURS))
                .status(EmergencyAccessStatus.ACTIVE)
                .build()
        );

        // Alert security team and department head
        notificationService.alertEmergencyAccess(access);

        // Grant temporary role via Keycloak Admin API
        keycloakAdminService.grantTemporaryRole(
            identity.getPrincipal().getName(),
            "emergency_access",
            Duration.ofHours(2)
        );

        return Response.ok(Map.of(
            "accessId", access.getId(),
            "expiresAt", access.getExpiresAt(),
            "message", "Emergency access granted. This access will be reviewed."
        )).build();
    }
}

5. アクセス制御マトリックス

###5.1.ヘルスケアの許可マトリックス

リソース医師看護師ラボテック薬剤師患者管理者
患者の人口統計R/WRRR自分:RR/W
臨床ノートR/WR--自分:R-
バイタルサインR/WR/W--自分:R-
ラボの注文R/WRR/WR自分:RR
ラボの結果RRR/W/VR自分:RR
処方箋R/WR-研究開発自分:RR
請求R---自分:RR/W
監査ログ-----R

R=読み取り、W=書き込み、D=ディスペンス、V=検証、Own=自分のレコードのみ

6. まとめ

このレッスンでは次のことを行います。

  • RBAC と ABAC およびヘルスケアの 4 つのレイヤーを組み合わせた戦略を比較します
  • Keycloak認可サービスの構成(リソース、ポリシー、権限)
  • Quarkus で RBAC を実装する @RolesAllowed
  • ABAC をカスタムで実装する SecurityIdentityAugmentor および認可インターセプター
  • ガラス破りの緊急アクセス手順を設計する

演習

  1. 検査結果へのアクセスに完全な ABAC を実装します。同じ科の医師のみが閲覧できます。
  2. 監査証跡と通知を備えたガラス破りワークフローを作成する
  3. 権限マトリックス (医師、看護師、患者のアクセス) の統合テストを作成する


◀ 前の記事次の記事 ▶
レッスン 5: 医療標準に合わせた Keycloak レルムの設計 - 病院向けのマルチテナントレッスン 7: FHIR の SMART - ヘルスケア API 用の OAuth2/OIDC