【kintone&GAS】一時的にアプリの管理者権限の付与と削除をするアプリを作る

当サイトではアフィリエイト広告を利用して商品を紹介しています。

kintoneを運用していて一番大変なのが開発よりも圧倒的にその後の保守と運用です。
今回はその作業を少しだけ楽にするツールをGASを使って作成します。
このツールを使うと以下のような作業が安全にかつ簡単に行えるようになります。

  • 一時的に自分をアプリ、レコード、フィールドすべての閲覧編集権限を付与する
  • Deploitで本番適用したあとに、開発環境用の確認用として設定しているグループやユーザーを本番環境のアプリから取り除く

行っていることの概要

  1. kintoneのレコードを保存したときに、Webhookを使いGASへ送信
  2. 受け取ったレコードの情報からkintoneのAPIを使って対象のアプリに対して権限の付与、または削除を実行

GASの設定

以下のコードをコピーしスクリプトに保存します。
下記の3点をスクリプトプロパティに設定してください。
その後スクリプトをデプロイします。デプロイ後のURLは後の工程で使うのでどこかにメモしておきましょう。

  • CYBOZU_USERNAME
  • CYBOZU_PASSWORD
  • CYBOZU_DOMAIN

class KintoneAppPermissions

class KintoneAppPermissions {
  constructor(appId) {
    this.appId = appId;
    const scriptProperties = PropertiesService.getScriptProperties();
    this.username = scriptProperties.getProperty('CYBOZU_USERNAME');
    this.password = scriptProperties.getProperty('CYBOZU_PASSWORD');
    this.domain = scriptProperties.getProperty('CYBOZU_DOMAIN');

    // アクセス権情報の取得
    this.appPermissions = this.getAppPermissions();
    this.recordPermissions = this.getRecordPermissions();
    this.fieldPermissions = this.getFieldPermissions();
  }

  /**
   * Base64エンコードされた認証ヘッダーを生成する。
   * @return {Object} 認証ヘッダー。
   */
  createAuthHeader() {
    const authString = `${this.username}:${this.password}`;
    const base64Auth = Utilities.base64Encode(authString);
    return {
      'X-Cybozu-Authorization': base64Auth
    };
  }
  /**
   * アプリのアクセス権情報を取得する。
   * @return {Object} アクセス権情報。
   */
  getAppPermissions() {
    const appPermissionsEndpoint = `https://${this.domain}/k/v1/app/acl.json?app=${this.appId}`;
    const options = {
      'method': 'GET',
      'headers': this.createAuthHeader(),
      'muteHttpExceptions': true
    };

    const response = UrlFetchApp.fetch(appPermissionsEndpoint, options);

    if (response.getResponseCode() !== 200) {
      throw new Error('Failed to get app permissions: ' + response.getContentText());
    }
    console.log("アプリのアクセス権を取得しました")
    return JSON.parse(response.getContentText());
  }
  /**
   * レコードのアクセス権情報を取得する。
   * @return {Object} レコードアクセス権情報。
   */
  getRecordPermissions() {
    // レコードのアクセス権取得用のエンドポイント
    const recordPermissionsEndpoint = `https://${this.domain}/k/v1/record/acl.json?app=${this.appId}`;

    const options = {
      'method': 'GET',
      'headers': this.createAuthHeader(),
      'muteHttpExceptions': true
    };

    const response = UrlFetchApp.fetch(recordPermissionsEndpoint, options);

    if (response.getResponseCode() !== 200) {
      throw new Error('Failed to get record permissions: ' + response.getContentText());
    }
    console.log("レコードのアクセス権を取得しました")
    return JSON.parse(response.getContentText());
  }
  /**
   * フィールドのアクセス権情報を取得する。
   * @return {Object} フィールドアクセス権情報。
   */
  getFieldPermissions() {
    const fieldPermissionsEndpoint = `https://${this.domain}/k/v1/field/acl.json?app=${this.appId}`;
    const options = {
      'method': 'GET',
      'headers': this.createAuthHeader(),
      'muteHttpExceptions': true
    };

    const response = UrlFetchApp.fetch(fieldPermissionsEndpoint, options);
    if (response.getResponseCode() !== 200) {
      throw new Error('Failed to get field permissions: ' + response.getContentText());
    }
    console.log("フィールドのアクセス権を取得しました")
    return JSON.parse(response.getContentText());
  }

  /**
   * 指定されたエンティティのアプリのアクセス権を削除します。
   * @param {Array<{code: string, type: string}>} deleteEntities - 削除するアクセス権のエンティティ配列。
   */
  appPermissionDelete(deleteEntities) {
    this.appPermissions.rights = this.appPermissions.rights.filter(permission =>
      !deleteEntities.some(entity => entity.code === permission.entity.code && entity.type === permission.entity.type));
  }

  /**
   * 指定されたエンティティのレコードのアクセス権を削除します。
   * @param {Array<{code: string, type: string}>} deleteEntities - 削除するアクセス権のエンティティ配列。
   */
  recordPermissionDelete(deleteEntities) {
    this.recordPermissions.rights.forEach(permission => {
      permission.entities = permission.entities.filter(entity =>
        !deleteEntities.some(delEntity => delEntity.code === entity.entity.code && delEntity.type === entity.entity.type));
    });
  }

  /**
   * 指定されたエンティティのフィールドのアクセス権を削除します。
   * @param {Array<{code: string, type: string}>} deleteEntities - 削除するアクセス権のエンティティ配列。
   */
  fieldPermissionDelete(deleteEntities) {
    this.fieldPermissions.rights.forEach(permission => {
      permission.entities = permission.entities.filter(entity =>
        !deleteEntities.some(delEntity => delEntity.code === entity.entity.code && delEntity.type === entity.entity.type));
    });
  }



  /**
   * 指定されたエンティティのアプリのアクセス権を追加します。
   * @param {Array<{code: string, type: string}>} addEntities - 追加するアクセス権のエンティティ配列。
   */
  appPermissionInsert(addEntities) {
    const newPermissions = addEntities.map(entity => {
      return {
        "entity": {
          "type": entity.type,
          "code": entity.code
        },
        "appEditable": true,
        "recordViewable": true,
        "recordAddable": true,
        "recordEditable": true,
      };
    });
    this.appPermissionDelete(addEntities); // 重複を避けるために先に削除
    this.appPermissions.rights = [...newPermissions,...this.appPermissions.rights];
  }

  /**
   * 指定されたエンティティのレコードのアクセス権を追加します。
   * @param {Array<{code: string, type: string}>} addEntities - 追加するアクセス権のエンティティ配列。
   */
  recordPermissionInsert(addEntities) {
    // 追加するエンティティのアクセス権を作成
    const newPermissions = addEntities.map(entity => {
      return {
        entity: {
          type: entity.type,
          code: entity.code
        },
        viewable: true,
        editable: true,
        deletable: false, // 必要に応じて設定
      };
    });

    this.recordPermissionDelete(addEntities)
    // 追加するエンティティのアクセス権を追加
    this.recordPermissions.rights.forEach(permission => {
      permission.entities = [...newPermissions, ...permission.entities];
    });
  }


  /**
   * 既存のフィールドに対して、指定されたエンティティのアクセス権を追加します。
   * @param {Array<{code: string, type: string}>} addEntities - 追加するアクセス権のエンティティ配列。
   */
  fieldPermissionInsert(addEntities) {
    const newPermissions = addEntities.map(entity => ({
      entity: { type: entity.type, code: entity.code },
      accessibility: 'WRITE' // 'READ' もしくは 'NONE' に変更可能
    }));

    this.fieldPermissionDelete(addEntities);

    this.fieldPermissions.rights.forEach(permission => {
      permission.entities = [...newPermissions, ...permission.entities];
    });
  }




  /**
   * アプリのアクセス権を更新し、最新の情報を再取得する。
   */
  updateAppPermissions() {
    const options = {
      'method': 'PUT',
      'headers': this.createAuthHeader(),
      'contentType': 'application/json',
      'payload': JSON.stringify({
        'app': this.appId,
        'rights': this.appPermissions.rights
      }),
      'muteHttpExceptions': true
    };

    const response = UrlFetchApp.fetch(`https://${this.domain}/k/v1/app/acl.json`, options);
    if (response.getResponseCode() !== 200) {
      throw new Error('Failed to update app permissions: ' + response.getContentText());
    }
    console.log("アプリのアクセス権を更新しました");
    this.appPermissions = this.getAppPermissions()

  }

  /**
   * レコードのアクセス権を更新し、最新の情報を再取得する。
   */
  updateRecordPermissions() {
    const options = {
      'method': 'PUT',
      'contentType': 'application/json',
      'headers': this.createAuthHeader(),
      'payload': JSON.stringify({
        'app': this.appId,
        'rights': this.recordPermissions.rights
      }),
      'muteHttpExceptions': true
    };

    const response = UrlFetchApp.fetch(`https://${this.domain}/k/v1/record/acl.json`, options);
    if (response.getResponseCode() !== 200) {
      throw new Error('Failed to update record permissions: ' + response.getContentText());
    }
    console.log("レコードのアクセス権を更新しました");
    this.recordPermissions = this.getRecordPermissions(); // 更新された権限情報を再取得
  }
  /**
   * フィールドのアクセス権を更新し、最新の情報を再取得する。
   */
  updateFieldPermissions() {
    const options = {
      'method': 'PUT',
      'contentType': 'application/json',
      'headers': this.createAuthHeader(),
      'payload': JSON.stringify({
        'app': this.appId,
        'rights': this.fieldPermissions.rights
      }),
      'muteHttpExceptions': true
    };

    const response = UrlFetchApp.fetch(`https://${this.domain}/k/v1/field/acl.json`, options);
    if (response.getResponseCode() !== 200) {
      throw new Error('Failed to update field permissions: ' + response.getContentText());
    }
    console.log("フィールドのアクセス権を更新しました");
    this.fieldPermissions = this.getFieldPermissions(); // 更新された権限情報を再取得
  }
}

doPost

function doPost(e) {
  // Webhookから送信されたデータをパース
  const webhookData = JSON.parse(e.postData.getDataAsString());
  const recordData = webhookData.record;

  // KintoneAppPermissionsクラスのインスタンスを作成
  const appId = recordData["アプリID"].value; // アプリIDを取得
  const kintonePermissions = new KintoneAppPermissions(appId);

  // 権限付与処理
  if (recordData["権限の付与_削除"].value.includes("権限付与")) {
    const addPermissions = recordData["権限付与"].value;
    const appPermissionsToAdd = [];
    const recordPermissionsToAdd = [];
    const fieldPermissionsToAdd = [];

    addPermissions.forEach(permissionRow => {
      const entityCode = permissionRow.value["権限付与_entityCode"].value;
      const entityType = permissionRow.value["権限付与_entityType"].value;
      const targets = permissionRow.value["権限付与_対象"].value;

      // 追加する権限のエンティティ
      const addEntity = { code: entityCode, type: entityType };

      // 各種権限タイプに対して配列に追加
      if (targets.includes("app")) {
        appPermissionsToAdd.push(addEntity);
      }
      if (targets.includes("record")) {
        recordPermissionsToAdd.push(addEntity);
      }
      if (targets.includes("field")) {
        fieldPermissionsToAdd.push(addEntity);
      }
    });

    // 一度に権限を追加して更新
    if (appPermissionsToAdd.length > 0) {
      kintonePermissions.appPermissionInsert(appPermissionsToAdd);
      kintonePermissions.updateAppPermissions();
    }
    if (recordPermissionsToAdd.length > 0) {
      kintonePermissions.recordPermissionInsert(recordPermissionsToAdd);
      kintonePermissions.updateRecordPermissions();
    }
    if (fieldPermissionsToAdd.length > 0) {
      kintonePermissions.fieldPermissionInsert(fieldPermissionsToAdd);
      kintonePermissions.updateFieldPermissions();
    }
  }



  // 権限削除処理
  if (recordData["権限の付与_削除"].value.includes("権限削除")) {
    const deletePermissions = recordData["権限削除"].value;
    const appPermissionsToDelete = [];
    const recordPermissionsToDelete = [];
    const fieldPermissionsToDelete = [];

    deletePermissions.forEach(permissionRow => {
      const entityCode = permissionRow.value["権限削除_entityCode"].value;
      const entityType = permissionRow.value["権限削除_entityType"].value;
      const targets = permissionRow.value["権限削除_対象"].value;

      // 削除する権限のエンティティ
      const deleteEntity = { code: entityCode, type: entityType };

      // 各種権限タイプに対して配列に追加
      if (targets.includes("app")) {
        appPermissionsToDelete.push(deleteEntity);
      }
      if (targets.includes("record")) {
        recordPermissionsToDelete.push(deleteEntity);
      }
      if (targets.includes("field")) {
        fieldPermissionsToDelete.push(deleteEntity);
      }
    });

    // 一度に権限を削除して更新
    if (appPermissionsToDelete.length > 0) {
      kintonePermissions.appPermissionDelete(appPermissionsToDelete);
      kintonePermissions.updateAppPermissions();
    }
    if (recordPermissionsToDelete.length > 0) {
      kintonePermissions.recordPermissionDelete(recordPermissionsToDelete);
      kintonePermissions.updateRecordPermissions();
    }
    if (fieldPermissionsToDelete.length > 0) {
      kintonePermissions.fieldPermissionDelete(fieldPermissionsToDelete);
      kintonePermissions.updateFieldPermissions();
    }
  }

}

kintoneのアプリの設定

kintoneのフィールドの設定は下記の画像のように設定します。
アプリのテンプレートをダウンロードできるようにしています。こちらをご利用ください。

webhook

先ほどGASのスクリプトで設定したURLを設定します。
通知を送信する条件は「レコードの編集時」にしてください。

設定後のkintoneの使い方

設定後は下記のように入力をします。
アプリIDは権限の設定を変更したいアプリのIDを入力してください。
※kintone上にアプリの一覧があればそこからルックアップをするのが良いと思います。

entityCode
アクセス権の設定対象のコード

entityType
「USER」 「GROUP」 「ORGANIZATION」「CREATOR」のいずれかを選びます。

対象
どこに権限の付与削除を行うかを選択します。

権限の付与_削除
付与をするのか削除をするのか選べます。両方を選べば両方保存時に実行されます

注意事項など

このアプリを触れると誰でも好きなアプリに好きな権限を付与できてしまうので、このアプリの置き場所や権限の設定は適切に管理するようにしてください。
使用したことに不具合などよる責任は負いかねますので、自己責任で使用してください。

ここまで読んでいただきありがとうございました。
質問感想はコメントやXまでおねがいいたします。
お役に立てたら嬉しいです。