メインコンテンツまでスキップ

ランタイム API (.NET)

Ninka.Authz は Ninka CLI が生成した bundle の .NET runtime です。compiled OPA WASM をインプロセスで評価し、ネットワークも、opa バイナリも、リクエスト経路上のポリシーサーバも要りません。対象は net8.0 で、バージョンを固定した Wasmtime パッケージに依存します。

dotnet add package Ninka.Authz

artifact_format_version 2 の Artifact Bundle をロードします。互換モードはありません。版のない manifest と build record は拒否します。古い artifact format には「成果物を再コンパイルせよ」、新しいものには「Ninka.Authz を更新せよ」と答えます。

C# Consumer Projection を生成する

.NET アプリケーションは、自分が commit する生成ファイル 1 つ、C# Consumer Projection から統合します。

npx ninka-authz build --out-csharp Generated/Ninka.g.cs

C# には解決すべき source root の慣習が無いため、--out-csharp がファイル自体を名指します。このファイルは実行 bundle を内部に抱え、Authz.FromEmbedded 経由で runtime に渡すため、配布する artifact ディレクトリも、実行時に読むディレクトリもありません。

using Ninka.Generated;

using var authz = NinkaProjection.Create();

各 policy の入力契約は i_<hash> という record です。policy id はデータなので、C# の識別子へは変換しません。一意になる id には別名 <Pascal>Input が付きます。policy reference 自体は、生成された Policies container に、policy id から導出した名前(ハイフンを取り除いた PascalCase)で置かれます。

projection は ninka/out/ ではなくあなたの source tree にあります。ninka/out/ の中に C# は 1 バイトもありません。ninka-authz verify --out-csharp <file> はその projection を再生成して比較するので、リリースが配るファイルはこれらの Tegata が projection した結果そのものです(再生成は上のコマンドです)。

Artifact をファイルとして配布する場合(任意)

artifact をファイルとして保持する tooling・script・アプリケーションは、代わりに Authz.Load(dir) を使い、ディレクトリを出力に含めます。

<ItemGroup>
<Content Include="ninka/out/**" CopyToOutputDirectory="PreserveNewest" />
</ItemGroup>

dotnet publishContent の項目を publish 出力に含めるため、コンパイル済みの bundle もアプリケーションの配備物に付いていきます。

class Authz

Authz.Load(dir, onInputError?, decisionLog?)

public static Authz Load(
string dir = "ninka/out",
Action<string, IReadOnlyList<string>>? onInputError = null,
DecisionLogOptions? decisionLog = null)

dir の built policies をロードし、各 runtime bundle を検証し、必要に応じて WASM を native code に compile して ready instance を返します。通常は DI container の singleton などで一度だけロードし、process lifetime で再利用します。

Load 時に検証すること

runtime は判定を提供する前に、各 bundle を検証します。実装されている検査には次が含まれます。

  • bundles/*.build.json の検出と、bundle_id とファイル名の一致
  • module が存在し、その SHA-256 が build record の wasm_sha256 と一致すること
  • 各 binding が名指す manifest が存在し、manifest.policy_id が一致すること
  • build record と全 manifest が、対応している artifact_format_version を持つこと。欠落は許容せず拒否する
  • manifest.rego_sha256 が必須であり、binding の rego_sha256 と一致すること
  • manifest.tegata_hash が存在し空でないこと(generated PolicyRef<T> を loaded bundle に束縛するために必要)
  • binding の entrypoint と policy 自身の manifest が宣言する entrypoint の一致、およびその名前が module の entrypoint table に存在すること。runtime は policy id から entrypoint 名を導出しません
  • host builtins を要求しないという契約を含む、OPA-WASM の互換性検査
  • policy id が一意であること:1 つの bundle の binding 群の中でも、bundle をまたいでも

いずれかが失敗すると decision を提供する前に NinkaLoadException を throw します。失敗は bundle 全体 のものです: binding が 1 つ壊れていればその bundle の全 policy が拒否され、拒否された load が既にインスタンス化した module は破棄されます。

Load は generated PolicyRef<T> と bundle の tegata_hash を比較しません。この時点では policy reference が渡されていないためです。この比較は Check で行います。ただし Load は loaded manifest すべてに空でない tegata_hash を要求します。bundle 側の hash は Load 時に必須であり、reference 側との照合は後段の Check が行う、という二段構えです。また Load は source Tegata を読まないため、source / artifact の STALE は ninka-authz verify や LIVE Authorization Reference が検出します。

Lifecycle と cost

初めて見る module の初回ロードでは、WASM から native code へのコンパイルが時間の大半を占めます。コンパイル済みの module は WASM の SHA-256 をキーにプロセス全体でキャッシュされるため、同じバイト列を再びロードしてもコンパイルし直さず再利用します。キャッシュが保持するのはコンパイル済みコードだけで、評価の状態は Authz のインスタンスごとに保たれます。

そのため、書き換わるポリシーの状態がインスタンス間で共有されることはなく、片方を dispose しても他方が無効になることはありません。module のキャッシュはプロセス全体で共有され、追い出しは行われないので、上限はそのプロセスがロードする WASM bundle の種類数で決まります。

Authz はインスタンスごとに native のリソースを持つので、所有者とともに dispose してください。通常の singleton であれば、プロセス終了時に DI コンテナが破棄するので十分です。

Check(policy, input)

public bool Check<TInput>(PolicyRef<TInput> policy, TInput input) where TInput : notnull

projection が生成する reference を使います。

using Ninka.Generated;

using var authz = NinkaProjection.Create();
bool allowed = authz.Check(Policies.InvoiceAccess, input);
if (!allowed) return Results.Forbid();

Policies.InvoiceAccess は policy を名指しし、input をその policy の generated C# contract(InvoiceAccessInput)に束縛します。reference は projection の生成元 compile の tegata_hash も保持します。Check はこれを loaded manifest と比較するため、別 compile の generated C# と bundle を混在させた場合、誤った contract で評価せず throw します。

serialize された最上位の input は JSON object でなければなりません。最上位が null・scalar・array の場合は caller contract 違反であり、policy 評価の前に throw します。object 内部の値の欠落や型違いは依然として authorization semantics の対象であり、API 形状のエラーにはなりません。

Input は serialize され、runtime 共通の 64-container-level depth limit を適用した後、loaded WASM に対して評価されます。Check は ALLOW なら true、DENY なら false を返します。deny-overrides は compiled policy 内で解決されます。未知のポリシー、reference と bundle のハッシュ不一致、input の serialize 失敗、WASM の失敗は、いずれも ALLOW への転落ではなく例外になります。

Checkthread-safe です。evaluation は policy instance ごとに serialize され、異なる loaded policy は並行評価できます。

Missing-input notification

onInputError は観測専用 channel です。判定が DENY で、由来ルールが required としている input key が欠落していれば、policy id と欠落したキーを callback に渡します。verdict は変更せず、callback exception は握りつぶされます。default は stderr に 1 行出力します。

DecisionLogOptions

Decision logging は default off です。有効にすると各 CheckDecisionLogEntry を 1 件出力します。これは意味論が同一であると明示的に定めた範囲において OPA Decision Log のフィールドモデルに従います。Ninka 固有の意味論と既知の逸脱は別に記録されており、フィールド名が一致することは、意味が一致することを含意しません。したがって「OPA 互換」とも呼べません。フィールドごとの説明は TypeScript reference にあります。両 runtime とも ARTIFACT_SPEC §5.6 が定めるエントリを出力するので、その説明はこちらにもそのまま当てはまります。

var authz = Authz.Load("ninka/out", decisionLog: new DecisionLogOptions
{
Enable = true,
Sink = entry => myLogger.LogInformation("{Entry}", JsonSerializer.Serialize(entry)),
});

エントリは ARTIFACT_SPEC §5.6 が定めるフィールドを持ちます。TypeScript runtime が出力するものと、集合も意味も同じです。

フィールド
decision_idunique decision id
timestampdecision timestamp
path"ninka/result"
resultboolean verdict
input評価した input を、宣言された AuthzInput の形へ射影し、そのうえで mask したもの
bundlespolicy → { revision }。revision は manifest の tegata_hash
labelsruntime / operational labels。policy_id(評価した policy を log 集計用の静的キーとして再掲)を含む
ninkaNinka-specific schema / policy verdict data

Logged input は 2 段階で作られます。まず宣言された AuthzInput の形(subject.properties / action.name / resource.type / resource.properties / context)へ射影され、そのうえで mask されます。manifest.decision_log.unmasked(vocabulary の decision_log.unmasked から導出。artifact v2 より前の名前は log_allowlist)にある attribute だけが raw で現れ、それ以外は "***" になります。action.nameresource.type は policy-selection literal として raw のままです。射影の形の外側にあるキーは、評価した input に存在していても mask されるのではなく entry から落ち、落ちたことを示すフィールドはありません。sink は observation-only で、throw しても decision は保持され callback failure は握りつぶされます。default sink は stdout に JSON 1 行です。

entry の field 集合は閉じていません。契約が名指していない field が存在しうることを consumer は許容しなければならず、知らない field があることを理由に entry を拒否・破棄してはいけません。互換 field は ninka.schemaartifact_format_version も上げずに追加されうるため、閉じた field set で検証する reader は通常の upgrade で決定ログを捨て始めます。

両 runtime が拘束されているのは仕様です。拘束の対象は次のとおりです。

  • フィールド集合
  • 各フィールドの意味
  • OPA フィールドモデルからの、宣言済みの逸脱
  • 射影と mask の規則(ARTIFACT_SPEC §5.6–§5.6.2)
  • 共有の validation / conformance corpus シリアライズした JSON がフィールド単位で同一であることは保証されません。射影の形の中で「値が無い / null である」ことを各言語がどう表すかは仕様も corpus も固定しておらず、2 つの runtime はそこで実際に異なります。エントリはフィールドで読んでください。2 つの runtime の出力を byte 単位で突き合わせる consumer は、何も固定していないものに依存していることになります。

PolicyIds

public IReadOnlyCollection<string> PolicyIds

loaded id の runtime introspection です。Application decision はこの string から policy selection を組み立てず、生成された Policies.<Name> reference を使います。

Dispose()

Authz は per-instance native Wasmtime resources を所有します。owner とともに dispose してください。

失敗契約

失敗モードが ALLOW を返すことはありません。

状況結果
Policy decision が DENY(required input 欠落を含む)false
inputnullArgumentNullException
最上位の input が null・scalar・array に serialize されるpolicy 評価の前に exception
Input が 64 container levels を超えるJsonException
Unknown policyNinkaUnknownPolicyException
PolicyRef<T>tegata_hash を持たないpolicy 評価の前に exception
PolicyRef<T>.tegata_hash が loaded manifest と不一致NinkaLoadException
manifest.tegata_hash の欠落・空文字load 時に NinkaLoadException
Bundle identity / integrity / format / compatibility failureload 時に NinkaLoadException
evaluation 中の WASM trap / abortexception。Authz instance は後続 check に利用可能

TypeScript runtime との相違

  • .NET の Check は同期です(Promise<boolean> ではなく bool を返します)。
  • どちらの runtime も同じ runtime bundle の契約を読み、同じ validation / conformance corpus に拘束されます。
  • Decision Log のフィールド集合、input の射影、mask の意味は ARTIFACT_SPEC §5.6–§5.6.2 が固定しており、どちらの runtime もそれを実装します。runtime を識別する label は異なり、シリアライズされた JSON がフィールド単位で同一であることは保証されません。

macOS では trap 処理がプロセス全体に効く

Ninka は macOS 上の Wasmtime engine を、POSIX シグナルによる trap 処理を使うように構成します。Wasmtime の既定である Mach port モードは、CoreCLR が必要とするスレッドごとの例外ポートと競合するためです。Wasmtime では trap の処理方式がプロセス全体で 1 つに決まるので、同じプロセスの中でアプリケーションが既定設定の Wasmtime Engine も作ると、2 つめの互換性のない設定が失敗します。同じプロセスで共存させる必要がある engine には macos_use_mach_ports = false を指定してください。

関連項目