見出し画像

kintoneゲストスペース用のグループを作ってかんたん権限設定

この記事は、kintone Advent Calendar 2022 の16日目の記事です。

こんにちは、かりんこラボの坂本です。
みなさん、ゲストスペース使っていますか?

主に社外の方と情報共有するための機能ですが、kintoneユーザー同士であればお互い追加費用もかからず使えるとっても便利な機能です。

「社内の環境と分離されていることにより、安全に情報共有ができる」ということが重要なのですが、このゲストスペースが完全に独立しているおかげで面倒なことがいくつかあります。

ゲストスペースでできないこと

まず、ゲストユーザーが利用できない機能は、下記ヘルプに詳しく記載されています。

これ以外で、アプリ作成・運用する上でとっても困ることが2つあります。

1.そのゲストスペース内でしかアプリ参照できない

ゲストスペース内アプリからそのスペース外アプリの参照、その逆のゲストスペース外アプリからゲストスペース内アプリへの参照ができません。

アプリのフィールドでは、ルックアップフィールド、関連レコードフィールドが関係します。それらは、そのゲストスペース外アプリが参照できません。(逆も同じ)
例えば、商品マスタアプリがゲストスペース外にあり、商品情報はそのマスタからルックアップしたいということが、実現できないということです。

また、「アクション」機能を使って別アプリに転記したいという場合も、ゲストスペースを超えてアクションを実行することができません。

2.グループ(ロール)がAdministratorsとEveryoneのみ & 組織は使えない

そもそも組織選択フィールドは、ゲストスペース内アプリには存在しません(フォームに配置できない)。
グループ選択フィールドは使用可能ですが、グループとして選択できるのは、AdministratorsとEveryoneのみです。

そして、重要なのは、アプリのアクセス権設定・通知設定・プロセス管理設定でも同様に、組織は選べず、グループもAdministratorsとEveryoneのみしか使えないということです。

ゲストスペースでグループ(ロール)・組織が使えないとアプリ設定が面倒!

通常、アクセス権・通知・プロセス管理で人を指定する場合、なるべく個人を指定するのではなく、グループ(ロール)や組織、フォームのフィールドを指定するのがオススメです。
個人指定してしまうと、異動等で担当が変わるときに毎回アプリの修正が必要になり、手間がかかりますし、設定漏れの可能性も高くなります。また時間が経つと個人にアクセス権を与えた経緯・根拠もあやふやになってくるなど、懸念点があるからです。

ですが、ゲストスペースでは組織は使えず、グループはAdministratorsとEveryoneのみなので、これら設定をほぼ個人指定で行う必要があります。

アクセス権で入力・表示制御を厳密に行いたいゲストスペースで、ユーザーが多かったり、入れ替わりが頻繁だったりする場合、これはかなり面倒です。

今回は、この問題を2つの設定用アプリ+カスタマイズで、アクセス権設定をなんとか乗り切る方法を考えてみました。

アプリ構成

【グループ(ロール)マスタ】
cybozu.comのグループ(ロール)のイメージで、グループとそれに属する人を設定するマスタアプリです。
1グループ1レコードです。

【アクセス権設定マスタ】
アプリ毎にアプリアクセス権とレコードアクセス権とフィールドアクセス権を設定するアプリです。
1アプリ1レコードです。

グループ(ロール)マスタで登録しているグループをルックアップし、ユーザーをコピーします。
「許可する操作」の選択肢はアプリ設定と合わせておきます。

レコードアクセス権のレコードの条件は、クエリ形式で入力します。クエリ形式については、developer networkの「レコードの一括取得(クエリで条件を指定)」ページを参照してください。

動作イメージ

1.グループ(ロール)マスタにグループとそれに属する人を登録します。
AdministratorsとEveryone(everyone)は、デフォルトのグループとしてユーザーを指定せず登録してください。

2.アクセス権設定マスタに権限を設定し保存後、更新ボタンをクリックします。

3.指定したアプリのそれぞれの権限がアクセス権設定マスタで設定した権限に書き換えられます。

【アプリのアクセス権】

アクセス権設定マスタ
権限確認アプリ(アプリID:520)の「アプリのアクセス権」

【レコードのアクセス権】

アクセス権設定マスタ
権限確認アプリ(アプリID:520)の「レコードのアクセス権」

【フィールドのアクセス権】

アクセス権設定マスタ
権限確認アプリ(アプリID:520)の「フィールドのアクセス権」

カスタマイズはこんな感じ

ボタン表示用に、以下のJavaScriptも追加してください。https://unpkg.com/kintone-ui-component/umd/kuc.min.js


/**
 * ゲストスペースのアクセス権設定
 * アクセス権だけでなく、動作テスト環境のアプリのすべての設定が運用環境のアプリに反映されます
 */
(() => {
    'use strict';

    /**
     * 権限更新
     */
    const updateAcl = async () => {
        const record = kintone.app.record.get().record;

        // アプリアクセス権
        const appRights = [];
        record['アプリアクセス権'].value.forEach((row) => {
            const appRight = {
                'appEditable': (row.value['許可する操作_アプリ'].value.indexOf('アプリ管理') !== -1),
                'recordViewable': (row.value['許可する操作_アプリ'].value.indexOf('レコード閲覧') !== -1),
                'recordAddable': (row.value['許可する操作_アプリ'].value.indexOf('レコード追加') !== -1),
                'recordEditable': (row.value['許可する操作_アプリ'].value.indexOf('レコード編集') !== -1),
                'recordDeletable': (row.value['許可する操作_アプリ'].value.indexOf('レコード削除') !== -1),
                'recordImportable': (row.value['許可する操作_アプリ'].value.indexOf('ファイル読み込み') !== -1),
                'recordExportable': (row.value['許可する操作_アプリ'].value.indexOf('ファイル書き出し') !== -1)
            };
            switch (row.value['ロール名_アプリ'].value) {
                case '':
                    break;
                case 'Administrators':
                case 'everyone': {
                    appRight['entity'] = {
                        'type': 'GROUP',
                        'code': row.value['ロール名_アプリ'].value
                    };
                    appRights.push(appRight);
                    break;
                }
                default:
                    row.value['ユーザー_アプリ'].value.forEach((usr) => {
                        const cloneRight = Object.assign({}, appRight);
                        cloneRight['entity'] = {
                            'type': 'USER',
                            'code': usr.code
                        };
                        appRights.push(cloneRight);
                    });
            }
        });
        if (appRights.length > 0) {
            const appBody = {
                'app': record['アプリID'].value,
                'rights': appRights,
                'revision': -1
            };
            try {
                const resp = await kintone.api(kintone.api.url('/k/v1/app/acl.json', true), 'PUT', appBody);
                console.log(resp);
            } catch (error) {
                console.error(error);
                alert('アプリアクセス権更新失敗!' + error.message);
                return;
            }
        }

        // レコードアクセス権
        const recordRights = [];
        record['レコードアクセス権'].value.forEach((row) => {
            const ability = {
                'viewable': (row.value['許可する操作_レコード'].value.indexOf('閲覧') !== -1),
                'editable': (row.value['許可する操作_レコード'].value.indexOf('編集') !== -1),
                'deletable': (row.value['許可する操作_レコード'].value.indexOf('削除') !== -1)
            };
            // 同じレコードの条件が設定されている場合は取得
            const matchedFilterCond = recordRights.filter((r) => {
                return r['filterCond'] === row.value['レコードの条件'].value;
            });
            const recordRight = {
                'filterCond': row.value['レコードの条件'].value,
                'entities': []
            };
            switch (row.value['ロール名_レコード'].value) {
                case '':
                    break;
                case 'Administrators':
                case 'everyone': {
                    const cloneRight = Object.assign({}, ability);
                    cloneRight['entity'] = {
                        'type': 'GROUP',
                        'code': row.value['ロール名_レコード'].value
                    };
                    if (matchedFilterCond.length === 0) {
                        recordRight.entities.push(cloneRight);
                        recordRights.push(recordRight);
                    } else {
                        matchedFilterCond[0].entities.push(cloneRight);
                    }
                    break;
                }
                default:
                    row.value['ユーザー_レコード'].value.forEach((usr) => {
                        const cloneRight = Object.assign({}, ability);
                        cloneRight['entity'] = {
                            'type': 'USER',
                            'code': usr.code
                        };
                        if (matchedFilterCond.length === 0) {
                            recordRight.entities.push(cloneRight);
                        } else {
                            matchedFilterCond[0].entities.push(cloneRight);
                        }
                    });
                    if (matchedFilterCond.length === 0) {
                        recordRights.push(recordRight);
                    }
            }
        });
        if (recordRights.length > 0) {
            const recordBody = {
                'app': record['アプリID'].value,
                'rights': recordRights,
                'revision': -1
            };
            try {
                const resp = await kintone.api(kintone.api.url('/k/v1/record/acl.json', true), 'PUT', recordBody);
                console.log(resp);
            } catch (error) {
                console.error(error);
                alert('レコードアクセス権更新失敗!' + error.message);
                return;
            }
        }

        // フィールドアクセス権
        const fieldRights = [];
        record['フィールドアクセス権'].value.forEach((row) => {
            const accessibility = () => {
                if (row.value['許可する操作_フィールド'].value.indexOf('編集') !== -1) {
                    return 'WRITE';
                } else if (row.value['許可する操作_フィールド'].value.indexOf('閲覧') !== -1) {
                    return 'READ';
                }
                return 'NONE';
            };
            // 同じフィールドコードが設定されている場合は取得
            const matchedCode = fieldRights.filter((r) => {
                return r['code'] === row.value['フィールドコード'].value;
            });
            const fieldRight = {
                'code': row.value['フィールドコード'].value,
                'entities': []
            };
            const entities = [];
            switch (row.value['ロール名_フィールド'].value) {
                case '':
                    break;
                case 'Administrators':
                case 'everyone': {
                    entities.push({
                        'type': 'GROUP',
                        'code': row.value['ロール名_フィールド'].value
                    });
                    break;
                }
                default:
                    row.value['ユーザー_フィールド'].value.forEach((usr) => {
                        entities.push({
                            'type': 'USER',
                            'code': usr.code
                        });
                    });
            }
            entities.forEach((entity) => {
                fieldRight.entities.push({
                    'accessibility': accessibility(),
                    'entity': entity
                });
            });
            if (entities.length > 0) {
                if (matchedCode.length === 0) {
                    fieldRights.push(fieldRight);
                } else {
                    fieldRight.entities.forEach((r) => {
                        matchedCode[0].entities.push(r);
                    });
                }
            }
        });
        if (fieldRights.length > 0) {
            const fieldBody = {
                'app': record['アプリID'].value,
                'rights': fieldRights,
                'revision': -1
            };
            try {
                const resp = await kintone.api(kintone.api.url('/k/v1/field/acl.json', true), 'PUT', fieldBody);
                console.log(resp);
            } catch (error) {
                console.error(error);
                alert('フィールドアクセス権更新失敗!' + error.message);
                return;
            }
        }
        alert('更新成功!');
    };

    /**
     * レコード詳細画面の表示後イベント
     */
    kintone.events.on('app.record.detail.show', (e) => {
        // 更新ボタンの作成
        const btnSubmit = new Kuc.Button({
            text: '更新',
            type: 'submit',
            id: 'submit-button'
        });
        btnSubmit.addEventListener('click', (event) => {
            updateAcl();
        });
        const sp = kintone.app.record.getSpaceElement('spButton');
        sp.appendChild(btnSubmit);
        return e;
    });
})();

使用しているAPIの詳細は、developer networkの下記ページを参照してください。

実運用で使うためには、もっと考慮・工夫が必要(バグもあるかも)ですが、参考のためにアプリテンプレートもダウンロードできるようにしておきます。

カスタマイズできる方は必要な処理を追加するなどコード改変して使ってみてください。

さいごに

kintone導入社数がどんどん増えている今日、ゲストスペース利用もさらに広がってくるのではないかと思っています。
ゲストスペースが完全に独立しているメリットはもちろんあるのですが、このためにアプリ設定が煩雑になるというのはなかなか悩ましい問題です。
できるなら、カスタマイズはしたくないので、ゲストスペースの機能アップデートに期待してます。