ランタイム 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 publish は Content の項目を 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が存在し空でないこと(generatedPolicyRef<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 への転落ではなく例外になります。
Check は thread-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 です。有効にすると各 Check が DecisionLogEntry を 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_id | unique decision id |
timestamp | decision timestamp |
path | "ninka/result" |
result | boolean verdict |
input | 評価した input を、宣言された AuthzInput の形へ射影し、そのうえで mask したもの |
bundles | policy → { revision }。revision は manifest の tegata_hash |
labels | runtime / operational labels。policy_id(評価した policy を log 集計用の静的キーとして再掲)を含む |
ninka | Ninka-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.name と resource.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.schema も artifact_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 |
input が null | ArgumentNullException |
最上位の input が null・scalar・array に serialize される | policy 評価の前に exception |
| Input が 64 container levels を超える | JsonException |
| Unknown policy | NinkaUnknownPolicyException |
PolicyRef<T> が tegata_hash を持たない | policy 評価の前に exception |
PolicyRef<T>.tegata_hash が loaded manifest と不一致 | NinkaLoadException |
manifest.tegata_hash の欠落・空文字 | load 時に NinkaLoadException |
| Bundle identity / integrity / format / compatibility failure | load 時に NinkaLoadException |
| evaluation 中の WASM trap / abort | exception。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 を指定してください。