0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

kintoneで退職者を利用停止にしても、アプリの設定には残る(2026年11月確認版)

0
Last updated at Posted at 2026-11-06

更新情報(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共通管理者で実行)。

kintone-user-refs.js
(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のプラグイン「異動・退職チェッカー」(無料)を作っています。全アプリをまとめて点検台帳にするなら、有料版の「棚卸し・変更監視パック」で同じ点検ができます。

関連記事

参考

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?