アクセス権運用効率化のためのアプリとカスタマイズ

著者名:山下 竜( サイボウズ株式会社 (External link)

目次

はじめに

kintone内のすべてのアプリを別のアプリで一元管理してみましょう。
本稿内では、これを「アプリ管理」と呼ぶことにします。
アプリ管理をすることで、アプリを一覧で把握でき、管理が容易になり、業務効率の向上が期待できます。
これは、全社のシステム管理をする情報システム部門や部門システム管理者の管理業務に役立ちます。
また、アプリ設定APIを活用すると、さらなる効率化を図ることが可能です。

今回はアプリのアクセス権を一覧化します。
本来アプリのアクセス権は各アプリの設定画面から確認・設定が可能ですが、一覧化することで、アクセス権の運用・管理を効率的に行うことが可能です。
カスタマイズでは、全アプリのアクセス権を取得し、あらかじめ設定したポリシーに違反しているアプリを絞りこむことをめざします。

アプリのアクセス権の管理を行うアプリ

できあがりのイメージ

kintoneのフィールド設定

まずは、アクセス権をレコードに保存するためのアプリを準備します。
設定するフィールドは次のとおりで、1アプリを1レコードで管理し、アクセス権をテーブルに保存する構成です。

「アプリID」フィールドはレコード更新APIを利用する際のkeyとして利用するので、重複禁止の設定を入れておきます。

「アクセス権のポリシー不適合」フィールドは後述しますが、アクセス権の設定ポリシーへの適合状況を管理するために利用します。

フィールド名 フィールド形式 フィールドコード 選択肢 その他必要な設定
アプリID 文字列(1行) アプリID 重複禁止
アプリ名 文字列(1行) アプリ名
アプリのアクセス権 テーブル アプリのアクセス権
アクセス権のポリシー不適合 チェックボックス アクセス権のポリシー不適合 アプリ管理、レコード閲覧、レコード追加、レコード編集、レコード削除、ファイル読み込み、ファイル書き出し

「アプリのアクセス権」のテーブル内フィールドは次のように設定します。
アプリのアクセス権の設定画面を模しています。
アプリ作成者のユーザーには「アプリ作成者」フィールドにチェックを入れて表現します。

フィールド名 フィールドタイプ フィールド形式 選択肢
ユーザー ユーザー選択 ユーザー
アプリ作成者 チェックボックス アプリ作成者 アプリ作成者
組織 組織選択 組織
グループ グループ選択 グループ
許可する操作 チェックボックス 許可する操作 レコード閲覧、レコード追加、レコード編集、レコード削除、アプリ管理、ファイル読み込み、ファイル書き出し、アクセス権の継承

アプリを使ったアクセス権運用

アクセス権運用のために、今回は大きく2つのカスタマイズを行います。

  • 複数アプリのアクセス権の設定状況を取得し、レコードに一括保存する。
  • 複数アプリのアクセス権の設定状況がポリシーに適合しているかチェックし、不適合の際には「アクセス権のポリシー不適合」のフィールドにチェックを入れる。

これらのカスタマイズによって、アクセス権の設定状況が一覧化され、確認しやすくなります。
また、ポリシーが不適合のアプリを絞り込んで適切な設定に更新するといった運用を効率的に実践できます。

アクセス権の設定ポリシーとして、今回2つのケースを例として紹介します。

  • アプリ管理者として2名以上のユーザーが設定されている。
  • Everyoneにはレコード削除させない。

これらのポリシーに不適合の場合は、「アクセス権のポリシー不適合」の「アプリ管理」や「レコード削除」フィールドにチェックを入れるようなカスタマイズを行います。

カスタマイズのコーディング例

今回のコーディング例ではロジックにフォーカスして、ローディングアイコンの設置や要素のスタイリングは行っていません。
また、アプリやレコードのAPI操作において全件処理に対応させていないので、必要に応じて @kintone/rest-api-client (External link) を利用する等で対応させてください。

アプリのアクセス権の保存

レコード一覧画面にアプリのアクセス権を取得するボタンを配置します。
そのボタンを押すと、アクセス権が保存されるようにします。
大まかな流れは以下のとおりです。

  1. 複数のアプリの情報を取得するAPI でアプリの情報一覧を取得する。
  2. アプリのアクセス権の設定を取得するAPI で各アプリのアクセス権情報を取得する。
  3. レコード更新のAPI を利用してレコードとして保存する。

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
/*
 * kintone to Excel sample program
 * Copyright (c) 2024 Cybozu
 *
 * Licensed under the MIT License
 * https://opensource.org/license/mit/
*/
(async () => {

  // アプリのアクセス権の辞書
  const permissions = {
    recordViewable: 'レコード閲覧',
    recordAddable: 'レコード追加',
    recordEditable: 'レコード編集',
    recordDeletable: 'レコード削除',
    appEditable: 'アプリ管理',
    recordExportable: 'ファイル読み込み',
    recordImportable: 'ファイル書き出し',
    includeSubs: 'アクセス権の継承'
  };

  // アプリのアクセス権を取得し、テーブルフィールドの値を作る
  const buildPermissionTable = async (app, _creator) => {
    const creator = _creator || (await kintone.api(kintone.api.url('/k/v1/app.json'), 'GET', {id: app})).creator;
    const {rights} = await kintone.api(kintone.api.url('/k/v1/app/acl.json'), 'GET', {app});
    const rows = rights.map(right => {
      return Object.entries(right).reduce((options, [label, value]) => {
        if (label === 'entity') {
          const {type, code} = value;
          switch (type) {
            case 'CREATOR':
              return {...options, ユーザー: {value: [{code: creator.code}]}, アプリ作成者: {value: ['アプリ作成者']}};
            case 'USER':
              return {...options, ユーザー: {value: [{code}]}};
            case 'ORGANIZATION':
              return {...options, 組織: {value: [{code}]}};
            case 'GROUP':
              return {...options, グループ: {value: [{code}]}};
            default:
              return {...options};
          }
        } else {
          return value ? {...options, 許可する操作: {value: [...options.許可する操作.value, permissions[label]]}} : {...options};
        }
      }, {
        ユーザー: {value: []},
        アプリ作成者: {value: []},
        組織: {value: []},
        グループ: {value: []},
        許可する操作: {value: []}
      });
    });
    return rows.map(row => ({value: row}));
  };

  kintone.events.on([
    'app.record.index.show',
  ], async (event) => {
    // レコード一覧画面にボタンを設置する
    if (document.querySelector('#app-permission-retrieval') !== null) {
      return event;
    }
    const headerMenuSpaceElement = kintone.app.getHeaderMenuSpaceElement();
    const button = document.createElement('button');
    button.id = 'app-permission-retrieval';
    button.textContent = 'アプリのアクセス権の取得';
    headerMenuSpaceElement.appendChild(button);

    // ボタン押下で複数アプリを取得して、アクセス権設定をレコード保存する
    button.addEventListener('click', async () => {
      try {
        const {apps} = await kintone.api(kintone.api.url('/k/v1/apps.json', true), 'GET', {});
        const records = await apps.reduce(async (promiseChain, {appId, name, creator}) => {
          const results = await promiseChain;
          const permissionTable = await buildPermissionTable(appId, creator);
          const record = {
            updateKey: {
              field: 'アプリID',
              value: appId
            },
            record: {
              アプリ名: {
                value: name
              },
              アプリのアクセス権: {
                value: permissionTable
              }
            }
          };
          return [...results, record];
        }, Promise.resolve([]));

        const response = await kintone.api('/k/v1/records.json', 'PUT', {
          app: kintone.app.getId(),
          upsert: true,
          records
        });
        console.log(response);
        alert('アプリのアクセス権を取得しました。');
        location.reload();
      } catch (error) {
        console.error('Error:', error);
        alert('アプリのアクセス権の取得に失敗しました。');
      }
    });
    return event;
  });

})();

buildPermissionTable()で各アプリのアクセス権取得からテーブルフィールドの値への整形しています。
アプリ作成者の情報をアクセス権取得のAPIは持っていないので、引数で与えられなければ取得するような形にしています(今回は引数が与えられていますが、この関数の汎用性確保のための記述です)。

ポリシー適合状況の確認

レコード一覧画面に「アプリアクセス権のポリシー適合状況確認」のボタンを配置します。
そのボタンを押下すると各アプリにおけるアクセス権ポリシー適合状況がチェックされます。
不適合の場合は、「アクセス権のポリシー不適合」のフィールドにチェックを入れるようにします。

アプリ管理権限に関するポリシー例

「アプリ管理者として2名以上のユーザーが設定されている」というポリシー例をチェックするカスタマイズは次のとおりです。
ロジックとしては、ここではシンプルにアプリ管理者が1名のみということを判定します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
/*
 * kintone to Excel sample program
 * Copyright (c) 2024 Cybozu
 *
 * Licensed under the MIT License
 * https://opensource.org/license/mit/
*/
(async () => {

  // アクセス権のポリシー適合状況を確認する
  const checkPermissionPolicy = record => {
    const rows = record.アプリのアクセス権.value;
    const managers = rows.reduce((accumulator, row) => {
      const {value: {許可する操作: {value: permissions}, ユーザー: {value: users}}} = row;
      if (permissions.includes('アプリ管理') && users.length === 1) {
        return [...accumulator, row];
      }
      return [...accumulator];

    }, []);
    return managers.length === 1;
  };

  // アクセス権のポリシー適合状況の結果からフィールドの値を作る
  const buildPolicyCheckResult = (record) => {
    const policy = 'アプリ管理';
    const isNotAppropriate = checkPermissionPolicy(record);
    const value = record.アクセス権のポリシー不適合.value.filter(option => option !== policy);
    return isNotAppropriate ? [...value, policy] : [...value];
  };

  kintone.events.on([
    'app.record.index.show',
  ], async (event) => {
    // レコード一覧画面にボタンを設置する
    if (document.querySelector('#check-app-permission-policy-1') !== null) {
      return event;
    }
    const headerMenuSpaceElement = kintone.app.getHeaderMenuSpaceElement();
    const button = document.createElement('button');
    button.id = 'check-app-permission-policy-1';
    button.textContent = 'アプリアクセス権のポリシー(アプリ管理)適合状況更新';
    headerMenuSpaceElement.appendChild(button);

    // ボタン押下でアクセス権のポリシー適合状況を確認し、結果をレコード更新する
    button.addEventListener('click', async () => {
      try {
        const {records: existingRecords} = await kintone.api(kintone.api.url('/k/v1/records.json', true), 'GET', {app: kintone.app.getId()});
        const records = existingRecords.map(record => (
          {
            updateKey: {
              field: 'アプリID',
              value: record.アプリID.value
            },
            record: {
              アクセス権のポリシー不適合: {
                value: buildPolicyCheckResult(record)
              }
            }
          }
        ));

        const response = await kintone.api('/k/v1/records.json', 'PUT', {
          app: kintone.app.getId(),
          upsert: true,
          records
        });
        console.log(response);
        alert('アプリのアクセス権のポリシー適合状況を更新しました。');
        location.reload();
      } catch (error) {
        console.error('Error:', error);
        alert('アプリのアクセス権のポリシー適合状況の更新に失敗しました。');
      }
    });
    return event;
  });

})();

レコード削除権限に関するポリシー例

「Everyoneにはレコード削除させない」というポリシー例をチェックするカスタマイズを見ていきたいと思います。
シンプルにEveryoneグループにレコード削除権限が含まれているかの判定をしています。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
/*
 * kintone to Excel sample program
 * Copyright (c) 2024 Cybozu
 *
 * Licensed under the MIT License
 * https://opensource.org/license/mit/
*/
(async () => {

  // アクセス権のポリシー適合状況を確認する
  const checkPermissionPolicy = record => {
    const rows = record.アプリのアクセス権.value;
    return rows.some(row => {
      const {value: {許可する操作: {value: permissions}, グループ: {value: [group] = []}}} = row;
      const code = group?.code;
      return (permissions.includes('レコード削除') && code === 'everyone');
    });
  };

  // アクセス権のポリシー適合状況の結果からフィールドの値を作る
  const buildPolicyCheckResult = (record) => {
    const policy = 'レコード削除';
    const isNotAppropriate = checkPermissionPolicy(record);
    const value = record.アクセス権のポリシー不適合.value.filter(option => option !== policy);
    return isNotAppropriate ? [...value, policy] : [...value];
  };

  kintone.events.on([
    'app.record.index.show',
  ], async (event) => {
    // レコード一覧画面にボタンを設置する
    if (document.querySelector('#check-app-permission-policy-2') !== null) {
      return event;
    }
    const headerMenuSpaceElement = kintone.app.getHeaderMenuSpaceElement();
    const button = document.createElement('button');
    button.id = 'check-app-permission-policy-2';
    button.textContent = 'アプリアクセス権のポリシー(レコード削除)適合状況更新';
    headerMenuSpaceElement.appendChild(button);

    // ボタン押下でアクセス権のポリシー適合状況を確認し、結果をレコード更新する
    button.addEventListener('click', async () => {
      try {
        const {records: existingRecords} = await kintone.api(kintone.api.url('/k/v1/records.json', true), 'GET', {app: kintone.app.getId()});
        const records = existingRecords.map(record => (
          {
            updateKey: {
              field: 'アプリID',
              value: record.アプリID.value
            },
            record: {
              アクセス権のポリシー不適合: {
                value: buildPolicyCheckResult(record)
              }
            }
          }
        ));

        const response = await kintone.api('/k/v1/records.json', 'PUT', {
          app: kintone.app.getId(),
          upsert: true,
          records
        });
        console.log(response);
        alert('アプリのアクセス権のポリシー適合状況を更新しました。');
        location.reload();
      } catch (error) {
        console.error('Error:', error);
        alert('アプリのアクセス権のポリシー適合状況の更新に失敗しました。');
      }
    });
    return event;
  });

})();

まとめ

アプリのアクセス権を管理するアプリのカスタマイズ例を紹介しました。
また、アクセス権のポリシーについて2つ例を紹介しましたが、自社のアプリ運用の考え方に応じたロジックを組み込んでいただければと思います。

本記事がアクセス権運用の効率化のヒントになれば幸いです。