API 専用ユーザー(画面ログイン不可・API キーのみ)
システム連携用に「画面にはログインしないが、API キーでだけ操作するユーザー」(サービスアカウント)を作りたい場合の検討です。
本体の標準機能ではありません
1.5.8.1 のプリザンターに API 専用ユーザーの機能はありません。以下は現行の実装(1.5.8.1)を前提にした改修案です。
前提にした現行の実装
認証の仕組みの詳細は 認証基盤(Cookie・API キー・Bearer) にあります。この検討に関係するのは次の点です。
| 項目 | 現行の動作 |
|---|---|
| API 専用のフラグ | 無い。Users の関連列は AllowApi(API の利用を許可)・ApiKey・Disabled・Lockout・Password |
「無効」(Disabled) | ログイン(GetByCredentials)と API キー認証(Context.GetUser)の両方を止める。「画面は不可・API は可」には使えない |
AllowApi | API の利用可否だけ。ログインには影響しない |
| API キーの発行・削除 | 「API 設定」画面で本人だけができる。UserUtilities.CreateApiKey / DeleteApiKey は対象を context.UserId に固定している |
UserSettings 列 | JSON の文字列。DB に書かれるのは RecordingJson が明示的に書き出すプロパティだけ |
API キーは「そのユーザーの分身」で、ユーザーから独立したサービスアカウントという考え方はありません。
案 1: 推測できないパスワードにする(改修なし)
- テナント管理者がユーザーを作り、「API の利用を許可」をオンにする
- 一時的なパスワードを設定する
- そのユーザーでログインし、「API 設定」画面で API キーを発行する
- パスワードを十分に長いランダムな文字列に変え、誰にも共有しない
- API キーを連携先に設定する
| 制約 | 内容 |
|---|---|
| API キーの再発行・削除 | 同じ手順で本人としてログインし直す必要がある |
| ログインの防止 | パスワードを知らなければ実質的にログインできないだけで、仕組みとしては止めていない |
| パスワード有効期限 | 期限が切れるとログイン時にパスワード変更を求められる(API には影響しない) |
| ライセンス数 | 有効なユーザーとして数えられる |
案 2: 管理者が API キーを代理で発行・削除できるようにする
テナント管理者(またはユーザー管理ができるユーザー)が、ユーザー編集画面から他のユーザーの API キーを発行・再発行・削除できるようにします。
| ファイル | 改修内容 |
|---|---|
UserUtilities.cs | CreateApiKey / DeleteApiKey で対象の userId を受け取る。ユーザー編集画面に API キーのタブを追加する |
UserValidators.cs | OnApiCreating / OnApiDeleting に、本人以外が対象のとき Permissions.CannotManageUsers と対象ユーザーの AllowApi を確かめる分岐を追加する |
UsersController.cs | 各アクションで対象の userId を受け取る |
改修イメージ
// UserUtilities.CreateApiKey(改修後のイメージ)
public static string CreateApiKey(Context context, SiteSettings ss, int userId = 0)
{
var targetUserId = userId > 0 ? userId : context.UserId;
var userModel = new UserModel(context: context, ss: ss, userId: targetUserId);
var invalid = UserValidators.OnApiCreating(
context: context,
userModel: userModel,
targetUserId: targetUserId);
// 以降は現行と同じ
}
// UserValidators.OnApiCreating(改修後のイメージ)
public static ErrorData OnApiCreating(
Context context, UserModel userModel, int targetUserId = 0)
{
if (!Parameters.Api.Enabled || context.ContractSettings.Api == false)
{
return new ErrorData(type: Error.Types.InvalidRequest);
}
if (targetUserId > 0 && targetUserId != context.UserId)
{
// 代理操作: ユーザー管理の権限と、対象ユーザーの AllowApi を確認
if (Permissions.CannotManageUsers(context: context))
{
return new ErrorData(type: Error.Types.HasNotPermission);
}
if (!userModel.AllowApi)
{
return new ErrorData(type: Error.Types.InvalidRequest);
}
}
else if (context.UserSettings?.AllowApi(context: context) == false)
{
return new ErrorData(type: Error.Types.InvalidRequest);
}
if (userModel.AccessStatus != Databases.AccessStatuses.Selected)
{
return new ErrorData(type: Error.Types.InvalidRequest);
}
return new ErrorData(type: Error.Types.None);
}現行の Permissions.CannotManageUsers は、EnableManageTenant(テナント管理者)でも特権ユーザーでもなく、Service.json の ShowProfiles も false のときに true を返します。
図を読み込み中…
案 3: UserSettings に ApiOnly フラグを追加する
UserSettings(JSON の列)に ApiOnly を追加し、ログインを明示的に拒否します。JSON の列なので DB のスキーマ変更(列の追加)は要りません。Newtonsoft.Json は既定で未知のプロパティを無視し、JSON に無いプロパティは既定値(bool? なら null)になるため、既存のデータとも互換性があります。
| ファイル | 改修内容 |
|---|---|
UserSettings.cs | public bool? ApiOnly; を追加し、RecordingJson に if (ApiOnly == true) us.ApiOnly = ApiOnly; を追加 |
UserModel.cs | ログインフォームの Authenticate(returnUrl 版)の先頭で ApiOnly のユーザーを拒否する |
UserUtilities.cs / UsersController.cs | テナント管理者が ApiOnly を切り替える画面とアクション |
改修イメージ
// UserModel.Authenticate(returnUrl 版)の先頭に追加するイメージ
if (IsApiOnlyUser(context: context))
{
return Deny(context: context); // 通常の認証失敗と同じ応答にする
}
private bool IsApiOnlyUser(Context context)
{
var userSettings = Repository.ExecuteScalar_string(
context: context,
statements: Rds.SelectUsers(
column: Rds.UsersColumn().UserSettings(),
where: Rds.UsersWhere()
.Add(
name: "LoginId",
value: LoginId,
raw: "(lower(\"Users\".\"LoginId\") = lower(@LoginId))")
.Disabled(false)))
?.Deserialize<UserSettings>();
return userSettings?.ApiOnly == true;
}
// テナント管理者が ApiOnly を切り替えるアクションのイメージ
public static string SetApiOnly(Context context, SiteSettings ss, int userId)
{
if (Permissions.CannotManageUsers(context: context))
{
return Messages.ResponseHasNotPermission(context: context).ToJson();
}
var userModel = new UserModel(context: context, ss: ss, userId: userId);
if (userModel.AccessStatus != Databases.AccessStatuses.Selected)
{
return Messages.ResponseNotFound(context: context).ToJson();
}
userModel.UserSettings.ApiOnly = context.Forms.Bool("ApiOnly");
Repository.ExecuteNonQuery(
context: context,
statements: Rds.UpdateUsers(
where: Rds.UsersWhere()
.TenantId(context.TenantId)
.UserId(userId),
param: Rds.UsersParam()
.UserSettings(userModel.UserSettings.RecordingJson()),
addUpdatorParam: false,
addUpdatedTimeParam: false));
return new ResponseCollection(context: context).ToJson();
}API キー認証(Context.GetUser)の条件は Disabled = false と Lockout = false だけで、UserSettings の中身は見ないため、ApiOnly = true でも API キー認証はそのまま使えます。
注意点:
ApiOnlyのユーザーは自分で「API 設定」画面を開けないため、案 2(管理者による代理発行)と組み合わせる必要があります。- ログインフォームを通らない経路(パスキー、SAML の SSO、IIS の Windows SSO、TrustedProxy 認証)も止めたい場合は、それぞれの経路にも同じチェックが要ります。パスキーは
UserModel.Authenticateを通るため先頭のチェックで止まりますが、SAML の SSO などはAllowAfterUrlで直接 Cookie を発行します(認証方式の内部動作)。 UserSettingsはセッションにも読み込まれるため、切り替えをすぐに反映させるにはセッションの扱いも考える必要があります。
比較
| 観点 | 案 1 | 案 2 | 案 3 |
|---|---|---|---|
| 本体の改修 | なし | 小(4 ファイル程度) | 小(3〜5 ファイル) |
| DB スキーマの変更 | なし | なし | なし(JSON の列を使う) |
| ログインの防止 | パスワードを秘匿するだけ | 同左 | フラグで拒否 |
| API キーの発行・再発行 | 本人としてログインが必要 | 管理者が代理でできる | 案 2 と組み合わせて管理者が行う |
| バージョンアップ時 | 影響なし | 改修の追従が必要 | 改修の追従が必要 |
画面ログインを確実に止めたいなら、案 2 と案 3 の組み合わせになります。