1. 序章

このチュートリアルでは、Springセキュリティの承認決定をOPA – Open PolicyAgentに外部化する方法を示します。

2. 前文:外部承認の場合

アプリケーション全体に共通する要件は、ポリシーに基づいて特定の決定を下す機能を持つことです。 このポリシーが十分に単純で変更される可能性が低い場合、このポリシーをコードで直接実装できます。これは最も一般的なシナリオです。

ただし、より柔軟性が必要な場合もあります。 アクセス制御の決定は一般的です。アプリケーションが複雑になるにつれて、特定の機能へのアクセスを許可するかどうかは、ユーザーだけでなく、要求の他のコンテキストの側面にも依存する可能性があります。 これらの側面には、IPアドレス、時刻、ログイン認証方法(例:「rememberme」、OTP)などが含まれる場合があります。

さらに、そのコンテキスト情報とユーザーのIDを組み合わせるルールは、できればアプリケーションのダウンタイムなしで、簡単に変更できる必要があります。 この要件は当然、専用サービスがポリシー評価要求を処理するアーキテクチャにつながります。

ここで、この柔軟性のトレードオフは、外部サービスへの呼び出しを行うために発生する複雑さとパフォーマンスの低下です。 一方、アプリケーションに影響を与えることなく、認証サービスを完全に進化させたり、置き換えたりすることもできます。 さらに、このサービスを複数のアプリケーションと共有できるため、アプリケーション間で一貫した承認モデルが可能になります。

3. OPAとは何ですか?

Open Policy Agent(略してOPA)は、Goに実装されているオープンソースのポリシー評価エンジンです。 当初はStyraによって開発され、現在はCNCFを卒業したプロジェクトです。 このツールの一般的な使用法のリストを次に示します。

  • エンボイ認証フィルター
  • Kubernetesアドミッションコントローラー
  • テラフォーム計画評価

OPAのインストールは非常に簡単です。プラットフォーム用のバイナリをダウンロードし、オペレーティングシステムのPATH内のフォルダーに配置するだけで、準備は完了です。 簡単なコマンドで正しくインストールされていることを確認できます。

$ opa version
Version: 0.39.0
Build Commit: cc965f6
Build Timestamp: 2022-03-31T12:34:56Z
Build Hostname: 5aba1d393f31
Go Version: go1.18
Platform: windows/amd64
WebAssembly: available

OPAは、複雑なオブジェクト構造でクエリを実行するように最適化された宣言型言語であるREGOで記述されたポリシーを評価します。 これらのクエリの結果は、特定のユースケースに従ってクライアントアプリケーションによって使用されます。 この場合、オブジェクト構造は承認リクエストであり、ポリシーを使用して結果をクエリし、特定の機能へのアクセスを許可します。

OPAのポリシーは一般的であり、承認の決定を表現するためにいかなる方法でも結び付けられていないことに注意することが重要です。 実際、これは、従来はDroolsなどのルールエンジンによって支配されていた他のシナリオで使用できます。

4. ポリシーの作成

これは、REGOで記述された単純な承認ポリシーがどのように見えるかです。

package baeldung.auth.account

# Not authorized by default
default authorized = false

authorized = true {
    count(deny) == 0
    count(allow) > 0
}

# Allow access to /public
allow["public"] {
    regex.match("^/public/.*",input.uri)
}

# Account API requires authenticated user
deny["account_api_authenticated"] {
    regex.match("^/account/.*",input.uri)
    regex.match("ANONYMOUS",input.principal)
}

# Authorize access to account
allow["account_api_authorized"] {
    regex.match("^/account/.+",input.uri)
    parts := split(input.uri,"/")
    account := parts[2]
    role := concat(":",[ "ROLE_account", "read", account] )
    role == input.authorities[i]
}

最初に気付くのはパッケージステートメントです。 OPAポリシーはパッケージを使用してルールを整理します。また、後で説明するように、受信リクエストを評価するときにも重要な役割を果たします。 ポリシーファイルを複数のディレクトリにまたがって整理できます。

次に、実際のポリシールールを定義します。

  • defaultルール。常にauthorized変数の値になります。
  • authorizedtrueであり、アクセスを拒否するルールがなく、アクセスを許可するルールが少なくとも1つある場合」と読み取ることができる主なアグリゲータールール
  • 許可ルールと拒否ルール。それぞれが一致した場合、allowまたはdeny配列にそれぞれエントリを追加する条件を表します。

OPAのポリシー言語の完全な説明はこの記事の範囲を超えていますが、ルール自体を読むのは難しくありません。 それらを見るときに覚えておくべきことがいくつかあります。

  • a:=bまたはa= b の形式のステートメントは単純な割り当てです(同じではありませんが
  • a = b{…条件}またはa{…条件}の形式のステートメントは、「baに割り当てる[X126X ]条件が真
  • ポリシードキュメントでの注文の表示は関係ありません

それ以外に、OPAには、文字列操作やコレクションなどのより使い慣れた機能に加えて、深くネストされたデータ構造のクエリ用に最適化された豊富な組み込み関数ライブラリが付属しています。

5. ポリシーの評価

前のセクションで定義したポリシーを使用して、承認リクエストを評価してみましょう。 この例では、着信リクエストの一部を含むJSON構造を使用して、この承認リクエストを作成します。

{
    "input": {
        "principal": "user1",
        "authorities": ["ROLE_account:read:0001"],
        "uri": "/account/0001",
        "headers": {
            "WebTestClient-Request-Id": "1",
            "Accept": "application/json"
        }
    }
}

リクエスト属性を単一のinputオブジェクトにラップしていることに注意してください。 このオブジェクトは、ポリシー評価中に input 変数になり、JavaScriptのような構文を使用してそのプロパティにアクセスできます。

ポリシーが期待どおりに機能するかどうかをテストするには、サーバーモードでローカルにOPAを実行し、いくつかのテストリクエストを手動で送信します。

$ opa run  -w -s src/test/rego

オプション-sはサーバーモードでの実行を有効にし、-wは自動ルールファイルの再読み込みを有効にします。 src / test / rego は、サンプルコードのポリシーファイルを含むフォルダーです。 実行されると、OPAはローカルポート8181でAPIリクエストをリッスンします。 必要に応じて、-aオプションを使用してデフォルトのポートを変更できます。

これで、curlまたはその他のツールを使用してリクエストを送信できます。

$ curl --location --request POST 'http://localhost:8181/v1/data/baeldung/auth/account' \
--header 'Content-Type: application/json' \
--data-raw '{
    "input": {
        "principal": "user1",
        "authorities": [],
        "uri": "/account/0001",
        "headers": {
            "WebTestClient-Request-Id": "1",
            "Accept": "application/json"
        }
    }
}'

/ v1 / dataプレフィックスの後のパス部分に注意してください。これはポリシーのパッケージ名に対応し、ドットがスラッシュに置き換えられています

応答は、入力データに対してポリシーを評価することによって生成されたすべての結果を含むJSONオブジェクトになります。

{
  "result": {
    "allow": [],
    "authorized": false,
    "deny": []
  }
}

result プロパティは、ポリシーエンジンによって生成された結果を含むオブジェクトです。 この場合、authorizedプロパティはfalseであることがわかります。 また、allowおよびdenyが空の配列であることがわかります。 これは、入力に一致する特定のルールがないことを意味します。 その結果、主要な承認済みルールも一致しませんでした。

6. SpringAuthorizationManagerの統合

OPAの動作を確認したので、次に進んでSpring認証フレームワークに統合できます。 ここでは、そのリアクティブWebバリアントに焦点を当てますが、一般的な考え方は通常のMVCベースのアプリケーションにも当てはまります

まず、バックエンドとしてOPAを使用するReactiveAuthorizationManagerBeanを実装する必要があります。

@Bean
public ReactiveAuthorizationManager<AuthorizationContext> opaAuthManager(WebClient opaWebClient) {
    
    return (auth, context) -> {
        return opaWebClient.post()
          .accept(MediaType.APPLICATION_JSON)
          .contentType(MediaType.APPLICATION_JSON)
          .body(toAuthorizationPayload(auth,context), Map.class)
          .exchangeToMono(this::toDecision);
    };
}

ここで、挿入された WebClient は別のBeanからのものであり、@ConfigurationPropretiesクラスからそのプロパティを事前に初期化します。

処理パイプラインは、 toAuthorizationRequest メソッドに、現在のAuthenticationおよびAuthorizationContextから情報を収集し、承認要求ペイロードを構築する義務を委任します。 同様に、 toAuthorizationDecision は承認応答を受け取り、それをAuthorizationDecisionにマップします。

ここで、このBeanを使用して SecurityWebFilterChain:を構築します。

@Bean
public SecurityWebFilterChain accountAuthorization(ServerHttpSecurity http, @Qualifier("opaWebClient") WebClient opaWebClient) {
    return http
      .httpBasic()
      .and()
      .authorizeExchange(exchanges -> {
          exchanges
            .pathMatchers("/account/*")
            .access(opaAuthManager(opaWebClient));
      })
      .build();
}

カスタムAuthorizationManager/accountAPIにのみ適用しています。 このアプローチの背後にある理由は、このロジックを簡単に拡張して複数のポリシードキュメントをサポートできるため、それらを保守しやすくするためです。 たとえば、リクエストURIを使用して適切なルールパッケージを選択し、この情報を使用して承認リクエストを作成する構成を作成できます。

この場合、 / account API自体は、偽の残高が設定されたAccountオブジェクトを返す単純なコントローラー/サービスペアです。

7. テスト

最後になりましたが、すべてをまとめるための統合テストを作成しましょう。 まず、「ハッピーパス」が機能することを確認しましょう。 これは、認証されたユーザーが与えられた場合、ユーザーは自分のアカウントにアクセスできる必要があることを意味します。

@Test
@WithMockUser(username = "user1", roles = { "account:read:0001"} )
void testGivenValidUser_thenSuccess() {
    rest.get()
     .uri("/account/0001")
      .accept(MediaType.APPLICATION_JSON)
      .exchange()
      .expectStatus()
      .is2xxSuccessful();
}

次に、認証されたユーザーが自分のアカウントにのみアクセスできるようにする必要があることも確認する必要があります。

@Test
@WithMockUser(username = "user1", roles = { "account:read:0002"} )
void testGivenValidUser_thenUnauthorized() {
    rest.get()
     .uri("/account/0001")
      .accept(MediaType.APPLICATION_JSON)
      .exchange()
      .expectStatus()
      .isForbidden();
}

最後に、認証されたユーザーに権限がない場合もテストしてみましょう。

@Test
@WithMockUser(username = "user1", roles = {} )
void testGivenNoAuthorities_thenForbidden() {
    rest.get()
      .uri("/account/0001")
      .accept(MediaType.APPLICATION_JSON)
      .exchange()
      .expectStatus()
      .isForbidden();
}

これらのテストは、IDEまたはコマンドラインから実行できます。いずれの場合も、最初に、承認ポリシーファイルを含むフォルダーを指すOPAサーバーを起動する必要があることに注意してください。

8. 結論

この記事では、OPAを使用してSpringセキュリティベースのアプリケーションの承認決定を外部化する方法を示しました。 いつものように、完全なコードはGitHubから入手できます。