UserPermission.Tool 0.1.0

dotnet tool install --global UserPermission.Tool --version 0.1.0
                    
This package contains a .NET tool you can call from the shell/command line.
dotnet new tool-manifest
                    
if you are setting up this repo
dotnet tool install --local UserPermission.Tool --version 0.1.0
                    
This package contains a .NET tool you can call from the shell/command line.
#tool dotnet:?package=UserPermission.Tool&version=0.1.0
                    
nuke :add-package UserPermission.Tool --version 0.1.0
                    

user-permission-cs

NuGet License NuGet Version NuGet Downloads

ユーザーとグループを管理するための非同期ライブラリ user-permission.NET (C#) バインディング です。

Rust 実装本体は別リポジトリにあります: mokuichi147/user-permission

このリポジトリは Rust の C ABI (cdylib) を P/Invoke で呼び出す薄いラッパーのみを含み、async/await でそのまま使える API を提供します。2 つの NuGet パッケージを公開します。

  • UserPermission — ライブラリ本体
  • UserPermission.Toolserve コマンドを持つ .NET ツール (dnx / dotnet tool)

クイックスタート (サーバーを試す)

.NET 10 SDK があれば、インストール不要で dnx(ワンショット実行)から同梱サーバーを直接起動できます。

dnx UserPermission.Tool serve --webui

従来の SDK では、グローバルツールとしてインストールして使えます。

dotnet tool install -g UserPermission.Tool
user-permission serve --webui

ブラウザで http://127.0.0.1:8000/ui を開くと Web 管理画面が利用できます。 オプション一覧は サーバー起動 を参照してください。

インストール

dotnet add package UserPermission

ネイティブライブラリ (user-permission-core の Rust コア) は各プラットフォーム向けに同梱されています。対応 RID:

OS アーキテクチャ RID
Linux x64 / arm64 linux-x64 / linux-arm64
macOS Intel / Apple Silicon osx-x64 / osx-arm64
Windows x64 win-x64

対応ターゲットフレームワーク

netstandard2.0net8.0 のマルチターゲットです。

  • .NET 8+ / .NET Core 3.1+ / .NET 5・6・7: ネイティブライブラリは runtimes/{rid}/native/ から自動解決されます(追加設定不要)。
  • .NET Framework 4.6.1+ など (netstandard2.0 経由): 参照・コンパイルは可能ですが、runtimes/ による RID 別ネイティブの自動配置をランタイムがサポートしないため、対象プラットフォームのネイティブライブラリ(例: user_permission_csharp.dll)を実行ファイルと同じディレクトリに手動配置するか PATH を通す必要があります。

使い方

初期化

using UserPermission;

// 初回実行時にシークレットキーを自動生成(以降はファイルから読み込み)
await using var db = await Database.ConnectAsync("app.db", secret: "secret.key");

User user = await db.Users.CreateAsync("alice", "password123");
Group group = await db.Groups.CreateAsync("admins");

Database.ConnectAsync(...) は生成と接続をまとめて行います。生成と接続を分けたい場合は次のようにします。

var db = new Database("app.db", secret: "secret.key");
await db.ConnectAsync();
// ...
await db.DisposeAsync();   // または using/await using で自動解放

最初に作成したユーザーは自動的に管理者グループ (admin) に所属し、管理者になります。

ユーザー管理

User user = await db.Users.CreateAsync("alice", "password123", displayName: "Alice");
User? byId = await db.Users.GetByIdAsync(1);
User? byName = await db.Users.GetByUsernameAsync("alice");
IReadOnlyList<User> users = await db.Users.ListAllAsync();

await db.Users.UpdateAsync(user.Id, password: "new_password");
await db.Users.UpdateAsync(user.Id, displayName: "Alice Smith");
await db.Users.UpdateAsync(user.Id, isActive: false);
await db.Users.DeleteAsync(user.Id);

認証・トークン

string? token = await db.LoginAsync("alice", "password123");
string? token2 = await db.LoginAsync("alice", "password123", expires: TimeSpan.FromHours(24));

// トークンを検証してユーザーを解決(無効・期限切れは null)
User? resolved = await db.VerifyTokenAndGetUserAsync(token);
Console.WriteLine(resolved!.Id);                       // ユーザーID
Console.WriteLine(resolved.Username);                  // ユーザー名
Console.WriteLine(await db.Users.IsAdminAsync(resolved.Id));  // bool

グループ管理

Group group = await db.Groups.CreateAsync("editors", description: "Editor group");
Group? byId = await db.Groups.GetByIdAsync(1);
Group? byName = await db.Groups.GetByNameAsync("editors");
IReadOnlyList<Group> groups = await db.Groups.ListAllAsync();
IReadOnlyList<Group> adminGroups = await db.Groups.ListAdminGroupsAsync();

await db.Groups.UpdateAsync(group.Id, description: "Updated description");
await db.Groups.DeleteAsync(group.Id);

グループメンバー管理

await db.Groups.AddUserAsync(group.Id, user.Id);
await db.Groups.RemoveUserAsync(group.Id, user.Id);
IReadOnlyList<User> members = await db.Groups.GetMembersAsync(group.Id);
IReadOnlyList<Group> userGroups = await db.Groups.GetUserGroupsAsync(user.Id);

サーバー起動

同梱の axum HTTP サーバーを起動できます。

using UserPermission;

await Server.ServeAsync(host: "0.0.0.0", port: 8001, prefix: "/api", webui: true);

ServeAsync はサーバーが停止するまで完了しない Task を返します。

引数 デフォルト 説明
database user_permission.db SQLiteデータベースのパス
secret secret.key シークレットキーファイルのパス
host 127.0.0.1 バインドアドレス
port 8000 バインドポート
prefix (なし) APIルートプレフィックス(例: /api
webui false Web管理画面を有効化
webuiPrefix /ui 管理画面のURLプレフィックス

CLI からも起動できます(UserPermission.Tool パッケージ)。

# .NET 10: インストール不要のワンショット実行
dnx UserPermission.Tool serve --host 0.0.0.0 --port 8001 --prefix /api --webui

# もしくはグローバルツールとして
dotnet tool install -g UserPermission.Tool
user-permission serve --host 0.0.0.0 --port 8001 --prefix /api --webui
オプション デフォルト 説明
--host 127.0.0.1 バインドアドレス
--port 8000 バインドポート
--database user_permission.db SQLiteデータベースのパス
--secret secret.key シークレットキーファイルのパス
--prefix (なし) APIルートプレフィックス(例: /api
--webui 無効 Web管理画面を有効化
--webui-prefix /ui 管理画面のURLプレフィックス

ユーザー管理などフル機能の CLI サーバーは Rust リポジトリ側にあります (cargo install user-permission)。この .NET ツールは serve のみを提供します。

リレー(中継)

Database に URL を渡すと、ローカル SQLite と中央サーバーを同じインターフェースで切り替えられます。

using UserPermission;

// 初期化は local / relay 共通。接続文字列だけを変える。
await using var local = await Database.ConnectAsync("app.db", secret: "secret.key"); // ファイルパス → ローカル SQLite
await using var relay = await Database.ConnectAsync("http://localhost:8001");         // URL → HTTP 中継

// 以降の操作はどちらの backend でも同一
string? token = await relay.LoginAsync("alice", "password123");
IReadOnlyList<User> users = await relay.Users.ListAllAsync();

db.LoginAsync(...) で取得したトークンは Database が内部に保持し、以降のリクエストの Authorization: Bearer に自動付与されます。

推奨: backend を意識しない実装。 認証 (LoginAsync / LoginServiceAsync)、トークン検証 (VerifyTokenAndGetUserAsync)、ユーザー・グループ操作はすべてローカル / リレーで同一の呼び出しで動作します。接続先 URL(またはファイルパス)を切り替えるだけで、アプリ側のコードを変えずにローカル運用と中央サーバー運用を行き来できます。

// どちらの backend でも同じコードが動く
async Task<User?> AuthenticateAsync(Database db, string username, string password)
{
    // login 失敗時は null、VerifyTokenAndGetUserAsync は null を渡すと null を返す
    string? token = await db.LoginAsync(username, password);
    return await db.VerifyTokenAndGetUserAsync(token);
}

サービスクライアントの管理操作だけは例外でローカル専用です(後述)。

サービスクライアント認証(client-credentials)

サービス間連携向けに、平文パスワードを持たずに読み取り専用のスコープ付きトークンを発行できます。 クライアントには users:read / groups:read のスコープのみ付与でき、書き込みや管理操作はできません。

using UserPermission;

// 管理側(ローカル)でサービスクライアントを発行。secret は発行時のみ取得可能。
await using (var admin = await Database.ConnectAsync("app.db", secret: "secret.key"))
{
    var (client, secret) = await admin.ServiceClients.CreateAsync("reader", new[] { Scopes.UsersRead });
    Console.WriteLine($"{client.ClientId} / {secret}");
}

// リレー側はサービストークンでログインし、スコープ内のみ読み取れる。
await using (var relay = await Database.ConnectAsync("http://localhost:8001"))
{
    await relay.LoginServiceAsync(clientId, secret);
    IReadOnlyList<User> users = await relay.Users.ListAllAsync();  // users:read があるので OK
}

注意: 管理操作はローカル backend 専用です。 ServiceClients.CreateAsync / ListAsync / GetByClientIdAsync / DeleteAsync / RotateSecretAsync はリレー(URL)backend で呼ぶと例外になります(サービスクライアントの発行・管理は中央サーバー側で行う設計のため)。一方、サービス認証 (db.LoginServiceAsync(...)) はローカル / リレーのどちらでも動作します。

スコープの検証は Scopes.Validate(...) で行えます(未知のスコープがあれば UserPermissionException を送出)。

Scopes.Validate(new[] { Scopes.UsersRead, Scopes.GroupsRead });

バックエンド非依存のユーティリティ

ローカル / リレーのどちらでも同じ呼び出しで動作します。

// トークンを検証してユーザーを解決(無効・期限切れ・サービストークンは null)
User? user = await db.VerifyTokenAndGetUserAsync(token);

// 管理者が不在なら作成して昇格(リレーでは no-op で null)
await db.BootstrapAdminIfNeededAsync("admin", "password123");

エラーハンドリング

操作が失敗すると UserPermissionException が送出されます。Kind プロパティで種別を判別できます。

try
{
    await db.Users.CreateAsync("alice", "password123");
}
catch (UserPermissionException ex) when (ex.Kind == UserPermissionErrorKind.Conflict)
{
    // ユーザー名が既に存在する
}

REST API・管理者ロール・スキーマ

REST エンドポイント仕様、groups.is_admin による管理者ロールの扱い、データベーススキーマは Rust リポジトリの README を参照してください。

アーキテクチャ

┌─────────────────────────┐
│ C# (UserPermission.dll) │  Database / UserManager / GroupManager / ...
│  P/Invoke ([DllImport])  │  ← async Task<T>(内部で Task.Run)
└────────────┬────────────┘
             │ C ABI(UTF-8文字列 + JSON エンベロープ)
┌────────────▼────────────────────────┐
│ Rust cdylib                          │  user_permission_csharp
│  (crates/user-permission-csharp)     │  ← tokio ランタイムで block_on
└────────────┬─────────────────────────┘
             │
┌────────────▼─────────────────────────┐
│ user-permission-core / user-permission│  crates.io(SQLite/Argon2/JWT/axum)
└──────────────────────────────────────┘
  • Rust 側は各関数が JSON エンベロープ ({"ok": …} / {"err": {"kind","message"}}) を返し、C# 側でデシリアライズ・例外変換します。
  • 非同期処理は Rust 側の共有 tokio ランタイムで block_on し、C# 側は Task.Run でラップして async API にしています。

開発

前提: .NET SDK 8.0+Rust toolchain

# 1. ネイティブライブラリ (cdylib) をビルド
cargo build --release            # もしくは開発中は cargo build(debug)

# 2. C# ライブラリのビルドとテスト
#    Directory.Build.targets が target/{profile}/ のネイティブ lib を
#    各プロジェクトの出力へ自動コピーするため、別途配置は不要。
dotnet test

リリース

v で始まる Git タグを push すると GitHub Actions が全プラットフォームのネイティブライブラリをビルドし、 NuGet パッケージ (UserPermissionUserPermission.Tool) を生成して nuget.org に公開します(詳細は .github/workflows/release.yml)。

ライセンス

MIT OR Apache-2.0

Product Compatible and additional computed target framework versions.
.NET net8.0 is compatible.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed.  net9.0 was computed.  net9.0-android was computed.  net9.0-browser was computed.  net9.0-ios was computed.  net9.0-maccatalyst was computed.  net9.0-macos was computed.  net9.0-tvos was computed.  net9.0-windows was computed.  net10.0 was computed.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

This package has no dependencies.

Version Downloads Last Updated
0.1.0 93 6/2/2026