更新情報(2026年11月10日):書き方を見直しました。検証の結果とスクリプトは変えていません。
はじめに
退職した人のユーザーを、cybozu.comで削除せずに「利用停止」にすることがあります。
検証用の環境で、アクセス権に入っているユーザーを利用停止にしてみました。アプリの設定を開くと、その人はアクセス権にそのまま残っていました。しかも、残ったままのアクセス権をAPIで保存し直そうとすると、エラーで止まります。
どこに残って、どこなら気づけるのか。順に確かめました。
結果
| 試したこと | 結果 |
|---|---|
| アクセス権に入っているユーザーを利用停止にする | 3つのアプリのアクセス権に、そのまま残った |
| 残ったアクセス権を、APIでそのまま保存し直す |
GAIA_IL26 で失敗 |
| 利用停止中のユーザーを、アクセス権や通知先に新しく指定する |
GAIA_IL26 で失敗 |
| 利用停止中のユーザーを、一覧の絞り込み条件に書く | 保存できた |
検証環境
- kintone(2026年11月6日時点)、cybozu.com共通管理者のユーザー。REST APIはブラウザのコンソールから実行
- アプリ1・3・5のアクセス権に入っているユーザー(kensho-admin)を、cybozu.com共通管理で「停止中」にして確かめ、終わったら「使用中」に戻した
- 保存し直す確認は、運用中の設定を変えないよう、プレビュー環境(
/k/v1/preview/app/acl.json)で行った
利用停止にしても、設定からは消えない
「停止中」にしても、3つのアプリのアクセス権にそのまま残りました。うち1つは、アプリの一覧のAPIに出てこないアプリです。
この状態のアクセス権を、APIでそのまま保存し直すとこうなります。
GAIA_IL26:指定したユーザー(code:kensho-admin)が見つかりません。
ユーザーを「使用中」に戻すと、保存できました。APIでアクセス権を書き換える仕組みを作っていると、退職者が1人残っているだけで止まります。
条件の中のユーザーは、保存のときに確かめられない
利用停止中のユーザーを新しく指定すると、アクセス権でも通知先でも GAIA_IL26 になりました。
一方で、一覧の絞り込み条件(作成者 in ("…"))には、そのまま保存できました。エラーにならないので、ここに残った指定は気づきにくいです。
ユーザーを指定できる場所は、アクセス権だけではない
アプリの設定で、ユーザーを指定できるのは次の場所です。退職や異動の前に見るのは、この全部です。
| 設定 | API(/k/v1/ 以下) |
指定する場所 |
|---|---|---|
| アクセス権 |
app/acl.json・record/acl.json・field/acl.json
|
対象。レコードのアクセス権は条件にも |
| プロセス管理 | app/status.json |
作業者、アクションの条件 |
| 通知 |
app/notifications/general.json・perRecord.json・reminder.json
|
通知先、条件 |
| フォーム | app/form/fields.json |
ユーザー選択の初期値と、選べる候補 |
| 一覧・グラフ |
app/views.json・app/reports.json
|
絞り込みの条件 |
| アクション | app/actions.json |
使える人 |
残った指定を洗い出すスクリプト
アプリの画面で、開発者ツールのコンソールに貼り付けて実行します。読み取るだけなので、設定は変わりません。ユーザーの状態は、cybozu.comのUser APIで確かめています(検証はcybozu.com共通管理者で実行)。
(async () => {
// 調べるアプリのID。いま開いているアプリだけなら、このまま
const APP_IDS = [kintone.app.getId()];
// ユーザーが入りうる設定(アクセス権・プロセス管理・通知・フォーム・一覧・グラフ・アクション)
const PLACES = {
'app/acl': 'アプリのアクセス権', 'record/acl': 'レコードのアクセス権', 'field/acl': 'フィールドのアクセス権',
'app/status': 'プロセス管理', 'app/notifications/general': 'アプリの条件通知',
'app/notifications/perRecord': 'レコードの条件通知', 'app/notifications/reminder': 'リマインダーの条件通知',
'app/form/fields': 'フォーム(初期値と候補)', 'app/views': '一覧', 'app/reports': 'グラフ', 'app/actions': 'アクション',
};
const USER_FIELDS = ['USER_SELECT', 'CREATOR', 'MODIFIER', 'STATUS_ASSIGNEE'];
const found = [];
for (const app of APP_IDS) {
const form = await kintone.api('/k/v1/app/form/fields.json', 'GET', { app }).catch((e) => console.warn(app, e.code));
if (!form) continue;
const userFields = Object.values(form.properties).filter((f) => USER_FIELDS.includes(f.type)).map((f) => f.code);
for (const [path, place] of Object.entries(PLACES)) {
const setting = await kintone.api(`/k/v1/${path}.json`, 'GET', { app }).catch((e) => console.warn(app, place, e.code));
if (!setting) continue;
const walk = (v, at) => {
if (Array.isArray(v)) return v.forEach((x, i) => walk(x, `${at}[${i}]`));
if (v && typeof v === 'object') {
// 1. { type: 'USER', code: 'xxx' } の形で入っているユーザー
if (v.type === 'USER' && v.code) found.push({ app, place, at, code: v.code });
return Object.entries(v).forEach(([k, x]) => walk(x, at ? `${at}.${k}` : k));
}
// 2. 条件の文字列(例:作成者 in ("yamada"))の中のユーザー
if (typeof v === 'string' && / in\s*\(/.test(v)) {
for (const [, field, list] of v.matchAll(/([^\s()]+)\s+(?:not\s+)?in\s*\(([^)]*)\)/g)) {
if (!userFields.includes(field)) continue;
for (const [, code] of list.matchAll(/"((?:[^"\\]|\\.)*)"/g)) found.push({ app, place, at, code });
}
}
};
walk(setting, '');
}
}
// 3. cybozu.com の User API で、いまの状態を確かめる
const codes = [...new Set(found.map((f) => f.code))];
const state = {};
for (let i = 0; i < codes.length; i += 100) {
const query = codes.slice(i, i + 100).map((c, j) => `codes[${j}]=${encodeURIComponent(c)}`).join('&');
const res = await fetch(`/v1/users.json?${query}`, { headers: { 'X-Requested-With': 'XMLHttpRequest' } }).then((r) => r.json());
for (const u of res.users) state[u.code] = u.valid ? '有効' : '利用停止中';
}
const rows = found.map((f) => ({ アプリ: f.app, 場所: f.place, 項目: f.at, ユーザー: f.code, 状態: state[f.code] || '見つからない' }));
console.table(rows.filter((r) => r.状態 !== '有効'));
console.log(`ユーザーの指定 ${rows.length}か所/うち、利用停止中か見つからないもの ${rows.filter((r) => r.状態 !== '有効').length}か所`);
})();
APP_IDS を検証環境のアプリ(1〜6)にして、ユーザーを停止中にしたまま実行した結果です。
| アプリ | 場所 | 項目 | ユーザー | 状態 |
|---|---|---|---|---|
| 1 | アプリのアクセス権 | rights[2].entity |
kensho-admin | 利用停止中 |
| 3 | アプリのアクセス権 | rights[1].entity |
kensho-admin | 利用停止中 |
| 5 | アプリのアクセス権 | rights[0].entity |
kensho-admin | 利用停止中 |
ユーザーの指定 4か所/うち、利用停止中か見つからないもの 3か所
※ 権限がなくて読めない設定(CB_NO02)と、ゲストスペースのアプリ(GAIA_IL23)は、警告を出して飛ばします。ゲストスペースのアプリは、送り先を /k/guest/(スペースID)/v1/ に変えてください。
※ ゲストユーザー(guest/ で始まる)はUser APIで取れないため、「見つからない」と出ます。
※ 組織とグループも同じ形(type が ORGANIZATION・GROUP)で入っています。/v1/organizations.json・/v1/groups.json で確かめられます。
※ プラグインの設定値と、JavaScriptカスタマイズの中身は対象外です。
さいごに
保存のときに弾かれるアクセス権より、黙って保存できてしまう一覧の条件のほうが厄介でした。
利用停止にする前に、その人の指定を組織やグループに置き換えておくと、あとで探さずに済みます。
筆者は、1つのアプリの設定に出てくるユーザー・組織・グループを一覧にし、利用停止中や見つからない指定を先頭に並べるkintoneのプラグイン「異動・退職チェッカー」(無料)を作っています。全アプリをまとめて点検台帳にするなら、有料版の「棚卸し・変更監視パック」で同じ点検ができます。
関連記事
- kintoneの全アプリの権限をAPIで洗い出したら、アプリが1つ足りなかった(2026年11月確認版)
- kintoneのプラグインが外部に送っていないかは、通信先のドメインだけでは分からない(2026年11月確認版)
- kintoneで、退職者や異動者が設定されているアプリを探す方法(台帳屋の使い方ガイド)