Google Workspaceの管理者ロールを期限付きで付与する承認ワークフローをGASで構築する

記事タイトルとURLをコピーする

G-gen の三浦です。当記事では、Google Apps Script(以下、GAS)を使い、申請と承認を経て Google Workspace の管理者ロールを有効期限付きで付与する仕組みを紹介します。

概要

Google Workspace の管理者ロール

Google Workspace では、ユーザーに管理者ロールを割り当てると、ロールに応じた管理作業を行えるようになります。管理者ロールには、Google があらかじめ用意した既定のロールと、権限を組み合わせて作成するカスタムロールがあります。

代表的な既定のロールは以下のとおりです。

管理者ロール できること
特権管理者 Google 管理コンソールと Admin API のすべての機能を使用する
ユーザー管理者 管理者以外のユーザーの作成・削除、名前やパスワードの変更などを行う
グループ管理者 Google グループの作成・削除、メンバーやアクセス設定の管理を行う
ヘルプデスク管理者 管理者以外のユーザーのパスワードを再設定する

管理者ロールを割り当てられるのは、特権管理者のみです。

一定期間有効な管理者ロールの割り当て

管理者ロールには、30 日などの定義済みの期間、あるいは任意の日時までの有効期限を設定できます。有効期限に達するとロールは自動的に取り消されるため、権限の取り消し漏れを防げます。主な制約は以下のとおりです。

制約 内容
期間の上限 最長 1 年
メイン管理者(後述) 有効期限付きの特権管理者ロールを持つユーザーは、メイン管理者に指定できない
同一対象への複数ロール 有効期限が異なる場合も含め、同じユーザー、グループ、サービスアカウントに複数のロールは割り当てられない
過去の日時 有効期限に指定できない

なおメイン管理者とは、機能の変更や支払いに関する通知など、Google からの重要な通知を受け取る 1 人の特権管理者のことです。管理コンソールの [アカウント] > [アカウント設定] > [プロフィール] の [連絡先情報] で確認できます。

期限付きの割り当ては、管理コンソールに加えて Admin SDK の Directory API でも設定できます。

当記事で構築する申請フロー

当記事では、一般ユーザーが必要なロールと期間を自ら申請できるよう、Google フォーム、スプレッドシート、GAS を組み合わせ、承認を経て管理者ロールを期限付きで割り当てる仕組みを構築します。

申請フロー

項番 実行者 内容
1 申請者 Google フォームで、ロール、期間、理由を入力して申請する
2 承認者 スプレッドシートで申請内容を確認し、承認する
3 GAS 有効期限付きでロールを割り当て、結果をスプレッドシートに記録する(自動)
4 Google Workspace 有効期限に達した管理者ロールを取り消す(自動)

GAS は承認したユーザーの権限で Directory API を呼び出すため、承認者は特権管理者である必要があります。

注意点

当記事のスクリプトは検証用の構成で、通知や申請の却下などの機能は含まれません。既存の割り当てとの重複も、ユーザーに直接割り当て済みの同じロールの場合のみ事前に確認します。本番環境では、組織の運用に合わせて改修してください。

なお「複数の関係者による承認」機能を使って API によるロールの割り当てを承認の対象にしている場合、GAS による割り当てはその場では完了せず、別の特権管理者による追加の承認が必要です。当記事の検証は、この設定を有効にしていない環境で実施しています。

検証の手順

当記事では、以下の流れで検証を行います。

項番 項目 内容
1 事前準備 スプレッドシートと GAS を作成し、申請対象のロールと申請フォームを準備します。
2 申請と承認 一般ユーザーがフォームで申請し、特権管理者が承認して管理者ロールを割り当てます。
3 自動取り消しの確認 有効期限に達したロールが自動的に取り消されることを確認します。

事前準備

スプレッドシートとスクリプトの作成

スプレッドシートの編集者はスクリプトも書き換えられるため、承認者のみがアクセスできる共有ドライブに作成します。空白部分を右クリックし、[Google スプレッドシート] > [空白のスプレッドシート] を選択します。

スプレッドシートの作成

スプレッドシートに任意の名前を設定し、[拡張機能] > [Apps Script] を選択します。

Apps Script を選択

プロジェクトに任意の名前を設定し、以下のコードを コード.gs に貼り付け、Ctrl + S を押して保存します。

/**
 * 管理者ロールの期限付き申請フロー
 * 空のスプレッドシートに紐付けて setup() を 1 回実行すると、申請フォームと台帳を作成する。
 * 承認メニューは「実行したユーザーの権限」で Directory API を呼ぶ。
 */
const SHEET_NAME = '申請台帳';
// フォームの「期間」の選択肢 → 時間
const DURATIONS = { '1時間': 1, '4時間': 4, '1日': 24, '3日': 72, '1週間': 168 };
// 申請できるロール:フォームの表示名 → roleName(listRoles() の出力から選んで追加する)
const ROLES = {
  '特権管理者': '_SEED_ADMIN_ROLE',
};
 
function onOpen() {
  SpreadsheetApp.getUi().createMenu('権限申請').addItem('選択行を承認して付与', 'approveSelected').addToUi();
}
 
// 初回のみ実行:フォームと台帳シートを作成する
function setup() {
  const ss = SpreadsheetApp.getActive();
  if (ss.getSheetByName(SHEET_NAME)) throw new Error('セットアップ済みです');
 
  const form = FormApp.create('管理者ロール申請').setCollectEmail(true).setAllowResponseEdits(false);
  form.addListItem().setTitle('ロール').setChoiceValues(Object.keys(ROLES)).setRequired(true);
  form.addMultipleChoiceItem().setTitle('期間').setChoiceValues(Object.keys(DURATIONS)).setRequired(true);
  form.addParagraphTextItem().setTitle('理由').setRequired(true);
  form.setDestination(FormApp.DestinationType.SPREADSHEET, ss.getId());
  SpreadsheetApp.flush();
 
  // 回答シート(A タイムスタンプ / B メールアドレス / C ロール / D 期間 / E 理由)の右に管理列を追加する
  const sheet = ss.getSheets().find((s) => s.getFormUrl() && FormApp.openByUrl(s.getFormUrl()).getId() === form.getId());
  sheet.setName(SHEET_NAME);
  sheet.getRange('F1:H1').setValues([['ステータス', '承認者', '有効期限']]);
  Logger.log(`申請フォーム:${form.getPublishedUrl()}`);
}
 
// テナントの事前定義ロールを ROLES に貼り付けられる形式で出力する
function listRoles() {
  systemRoles_().forEach((r) => Logger.log(`'': '${r.roleName}', // ${r.roleDescription || ''}`));
}
 
function approveSelected() {
  const ui = SpreadsheetApp.getUi();
  const sheet = SpreadsheetApp.getActiveSheet();
  const range = sheet.getActiveRange();
  const row = range.getRow();
  if (sheet.getName() !== SHEET_NAME || row < 2 || range.getNumRows() !== 1) {
    throw new Error('申請台帳の行を 1 行だけ選択してから実行してください');
  }
 
  const [, requester, roleLabel, duration, , status] = sheet.getRange(row, 1, 1, 6).getValues()[0];
  const approver = Session.getActiveUser().getEmail();
  if (status) throw new Error(`処理済みの申請です:${status}`);
  if (!approver) throw new Error('承認者を特定できません');
  // エイリアスや大文字小文字の違いで迂回されないよう、ユーザー ID で本人承認を判定する
  const requesterUser = AdminDirectory.Users.get(requester);
  const approverUser = AdminDirectory.Users.get(approver);
  if (!approverUser.isAdmin) throw new Error('管理者ロールは、特権管理者のみ承認できます');
  if (requesterUser.id === approverUser.id) throw new Error('申請者本人は承認できません');
 
  const role = systemRoles_().find((r) => r.roleName === ROLES[roleLabel]);
  const hours = DURATIONS[duration];
  if (!role || !hours) throw new Error(`申請内容が不正です:${roleLabel} / ${duration}`);
 
  const ok = ui.alert('確認', `${requester} に「${roleLabel}」を ${hours} 時間付与します。`, ui.ButtonSet.OK_CANCEL);
  if (ok !== ui.Button.OK) return;
 
  // 確認ダイアログの表示中に他の承認者が処理した場合に備え、ロックを取得して状態を再確認する
  const lock = LockService.getDocumentLock();
  lock.waitLock(30000);
  try {
    const current = sheet.getRange(row, 6).getValue();
    if (current) throw new Error(`処理済みの申請です:${current}`);
    // 台帳への記録漏れなどで同じロールを二重に割り当てないよう、既存の割り当てを確認する
    const assigned = listAll_((page) => AdminDirectory.RoleAssignments.list('my_customer', { userKey: requesterUser.id, ...page }));
    if (assigned.some((a) => a.roleId === role.roleId)) {
      throw new Error('既に同じロールが割り当てられています。管理コンソールで割り当て状況を確認してください');
    }
 
    const res = AdminDirectory.RoleAssignments.insert({
      roleId: role.roleId,
      assignedTo: requesterUser.id,
      scopeType: 'CUSTOMER',
      expirationDetails: { expireTime: new Date(Date.now() + hours * 3600e3).toISOString() },
    }, 'my_customer');
    // 有効期限のない割り当ては恒久的な権限になるため、直ちに取り消す
    if (!res.expirationDetails || !res.expirationDetails.expireTime) {
      try {
        AdminDirectory.RoleAssignments.remove('my_customer', res.roleAssignmentId);
      } catch (e) {
        throw new Error(`有効期限なしで割り当てられ、取り消しにも失敗しました。管理コンソールで直ちに確認してください(割り当て ID:${res.roleAssignmentId})`);
      }
      throw new Error('有効期限が設定されなかったため、割り当てを取り消しました');
    }
    sheet.getRange(row, 6, 1, 3).setValues([['付与済み', approver, new Date(res.expirationDetails.expireTime)]]);
    console.log(`付与:${requester} / ${role.roleName} / 承認者:${approver} / 割り当て ID:${res.roleAssignmentId}`);
  } finally {
    lock.releaseLock();
  }
}
 
// テナントの事前定義ロール(ロール名に対応する roleId を API から取得する)
function systemRoles_() {
  return listAll_((page) => AdminDirectory.Roles.list('my_customer', { maxResults: 100, ...page })).filter((r) => r.isSystemRole);
}
 
// ページングされた一覧をすべて取得する(2 ページ目以降のみ pageToken を渡す)
function listAll_(fetchPage) {
  const items = [];
  let pageToken;
  do {
    const res = fetchPage(pageToken ? { pageToken } : {});
    items.push(...(res.items || []));
    pageToken = res.nextPageToken;
  } while (pageToken);
  return items;
}

コードを保存

Admin SDK の拡張サービスの有効化

管理者ロールの割り当てに Admin SDK API を使用するため、拡張サービスとして追加します。[サービス] > [Admin SDK API] を選択し、[追加] を選択します。

Admin SDK API の追加

申請対象のロールの選択

サービスの下に AdminDirectory が追加されたことを確認し、関数を [listRoles] に設定し、[実行] を選択します。

listRoles の実行

スクリプトの実行に必要なアクセスの許可(認可)を求められるため、[権限を確認] を選択します。

権限を確認を選択

[次へ] を選択し、要求されるアクセス権限を確認してチェックを入れ、[次へ] を選択します。

次へを選択
権限の確認と選択

認可が完了すると、実行ログに既定のロールの一覧が表示されます。申請対象のロール名(_ で始まる値)を控えておきます。ロールの割り当てに必要なロール ID は、スクリプトがロール名から取得します。

ロール名の確認

コード.gs の 10 行目の ROLES に申請対象のロールを記載し、保存します。左側はフォームの選択肢に表示する名前、右側は listRoles で確認したロール名です(カスタムロールは対象外)。当検証では、概要で紹介した 4 つのロールを対象とします。

const ROLES = {
  '特権管理者': '_SEED_ADMIN_ROLE',
  'ユーザー管理者': '_USER_MANAGEMENT_ADMIN_ROLE',
  'グループ管理者': '_GROUPS_ADMIN_ROLE',
  'ヘルプデスク管理者': '_HELP_DESK_ADMIN_ROLE',
};

コードの更新

申請フォームと申請台帳の作成

関数を [setup] に設定し、[実行] を選択します。

setup の実行

実行ログに Google フォームの URL が表示されるため、ブラウザでアクセスします。

スクリプトによる Google フォームの生成

右下の編集アイコンからフォームの編集画面を開きます。

フォームの編集を選択

他のユーザーのメールアドレスで申請されないよう、[設定] > [回答] の [メールアドレスを収集する] を [確認済み] に変更します。回答者がログインしている Google アカウントのメールアドレスが記録され、GAS はこれを付与先として使用します。

メールアドレスの収集設定の有効化

右上の共有アイコンを選択し、[一般的なアクセス] を以下のとおり設定して [完了] を選択します。[回答者ビュー] は組織外からの申請を防ぐため、[編集者ビュー] は申請者にフォームの設定を変更されないための設定です。

項目 設定値
回答者ビュー 組織名
編集者ビュー 制限付き

共有設定状況の確認

フォームは作成者のマイドライブに作成され、作成者のアカウントを削除すると、他のユーザーへ移行しない限り削除されます。そのため、スプレッドシートと同様に承認者のみがアクセスできる共有ドライブへ移動します。

検証

一般ユーザーによる申請の実施

管理者ロールを持たない検証用のユーザーで、フォームにアクセスします。

検証ユーザーの確認

以下のとおり選択・入力し、[送信] を選択します。

項目 入力値
ロール グループ管理者
期間 1時間
理由 グループメンバーの棚卸のため

権限付与申請の実施

管理者による申請の承認

スプレッドシートの [申請台帳] シートに、申請が記録されていることを確認します。付与対象のメールアドレスのセルを選択し、[権限申請] > [選択行を承認して付与] を選択します。メニューが表示されていない場合は、スプレッドシートを再読み込みします。

管理者による承認の実施

内容を確認し、[OK] を選択します。

付与するロールと期間の確認

ステータス、承認者、有効期限が更新されたことを確認します。

ステータスの更新確認

有効期限は日付のみ表示されます。時刻も表示する場合は、有効期限の列を選択し、[表示形式] > [数字] > [日時] を選択します。

表示形式の変更
表示形式の変更確認

権限付与の確認

管理コンソールで、対象のユーザーにロールが有効期限付きで割り当てられていることを確認します。

権限付与の確認

有効期限による自動取り消しの確認

有効期限を過ぎた後、管理コンソールで対象のユーザーのロールが取り消されていることを確認します。

ロールの取り消しの確認

管理コンソールの [レポート] > [監査と調査] > [管理ログイベント] で、有効期限の時刻にロールが取り消されていることを確認します。

取り消しログの確認

三浦 健斗 (記事一覧)

クラウドソリューション部

2023年10月よりG-genにジョイン。元オンプレ中心のネットワークエンジニア。 ネットワーク・セキュリティ・唐揚げ・辛いものが好き。

Google Cloud Partner All Certification Holders 2025 / Google Cloud Partner Top Engineer 2026