👨‍🔬

組織の異動や退場 または私は如何にして心配するのを止めてGoogleグループを削除するようになったか

2023/12/01に公開

この記事は MICIN Advent Calendar 2023 の 1 日目の記事です。

はじめに

弊社株式会社 MICIN には、2023 年 12 月現在 209 の Google グループがあります。
絶対数としてそこまで数が多いわけではないのですが、過去存在していたが現在は無くなった部署やプロジェクトのグループや、誰にも紐づいていないグループ、あるいは 1 人しか紐づいていないグループが点在しています。
また現状すでに新規メンバーが Google ドライブの権限設定やカレンダー招待にどの Google グループを使用すればいいか分からない、あるいは既存メンバーすら過去の部署グループを使い続けるなど一部で混乱が生じています。

今後の組織改編や新プロジェクト発足など、さらなる会社成長に沿って Google グループを作り続けると将来的な破綻は避けられなさそうですので、今のうちに手を打っておきたいところです。

Google グループ削除の難しさ

Google グループは以下のような機能を持っています。

  • メーリングリスト
  • Google カレンダーの予定招待
  • Google ドライブの共有権限設定
  • Google Cloud Platform IAM のメンバー設定
  • その他、SaaS によっては Google Workspace を ID プロバイダーとして連携し、Google グループで権限管理する機能があるようです

Google グループは様々な用途で使用されているため、仮にメーリングリストで使われていないことが明白だとしても、実は Google ドライブの共有に使われていた、カレンダーの予定共有に使われていた、ということが生じます。

また、Google グループは一度削除すると元に戻せません。

削除したグループを復元することはできません。メンバーはグループで共有されたファイルなどにアクセスできなくなり、そのグループのアドレス宛に送信されたメールも配信されません。

https://support.google.com/a/answer/9664762?hl=ja#:~:text=グループを作成したの,復元することはできません。

誤って削除すると「取引先からのメールが届かなくなった」「ドライブのファイルが開けない」や「カレンダーの予定が消えた」など混乱必至です。

そして、Google Workspace の管理画面には Google グループそれぞれの機能の利用状況を確認するような画面が存在しません。

対応方針

1. 既存 Google グループがどの用途で利用されているか棚卸し
Google グループの各機能の利用状況確認は Google Workspace 管理画面からは確認できませんが、Google Apps Script を用いることで取得できます。
それぞれの機能について取得し、現状を棚卸ししていきます。

2. 古い Google グループに紐づく Google ドライブ共有設定、Google カレンダー予定を、現行の Google グループに移管
古い部署・プロジェクトの Google グループに紐づくドライブの共有設定やカレンダーの予定を、現行部署・プロジェクトの Google グループに移管します。
移管先グループの推定に Google グループ間の近似を計測しつつ、最終的には各部・各プロジェクトの確認が必要になりそうです。

3. Google グループのルール・ガイドライン策定
これまでも新しい Google グループ作成・変更・削除は情報セキュリティ部への申請の上、情報セキュリティ部が作成するフローにはなっていました。
しかし削除の申請は過去一度も行われたことがありません。
新たに削除に関するルール・ガイドラインを策定することで、Google グループ数の膨張を防ぎます。

1. 既存 Google グループがどの用途で利用されているか棚卸し

1.1. Google グループ一覧取得

  • サービス AdminDirectory を追加してください
function getGoogleGroups() {
  const domainName = "example.com"; // 取得グループのドメイン
  const maxResults = 200; // maxResults のデフォルトは200
  let pageToken = "";

  const spreadsheet = SpreadsheetApp.getActiveSpreadsheet();
  const resultSheetName =
    Utilities.formatDate(new Date(), "JST", "yyyyMMddHHmmss") + "_result";
  const resultSheet = spreadsheet.insertSheet(resultSheetName);

  let values = [];

  // ヘッダー追加
  const header = [
    "グループ名",
    "メールアドレス",
    "説明",
    "メンバー数",
    "グループメンバー",
  ];
  values.push(header);

  do {
    const groupsList = AdminDirectory.Groups.list({
      domain: domainName,
      maxResults: maxResults,
      pageToken: pageToken,
    }); //グループ一覧の取得

    for (let i = 0; i < groupsList.groups.length; i++) {
      const groupName = groupsList.groups[i].name; // グループ名
      const email = groupsList.groups[i].email; // メールアドレス
      const description = groupsList.groups[i].description; // 説明
      const membersCount = groupsList.groups[i].directMembersCount; // メンバー数

      // グループのメンバー情報の取得
      const members = AdminDirectory.Members.list(
        groupsList.groups[i].email
      ).members;
      const membersStr =
        members?.map((member) => member.email)?.join(",") ?? "";

      values.push([groupName, email, description, membersCount, membersStr]);
    }

    pageToken = groupsList["nextPageToken"];
  } while (pageToken);

  resultSheet.getRange(1, 1, values.length, header.length).setValues(values);
}

1.2. メーリングリスト

  • Google スプレッドシートにシート「GoogleGroupList」を作成し、A 列に取得したい Google グループのアドレスを記入してください
  • サービス Gmail を追加してください
  • 肝は GmailApp.search で指定したクエリになります
    • -filename:vcs -filename:ics で Google カレンダーの招待通知を取得対象外にしています(Google カレンダーに直近招待されているかを確認したい場合はこの条件を外してください)
    • replyto:google-workspace-alerts-noreply@google.com で Google Workspace からの通知を取得対象外にしています
function getRecentEmails() {
  const spreadsheet = SpreadsheetApp.getActiveSpreadsheet();

  // 対象Googleグループアドレスを取得
  const googleGroupListSheet = spreadsheet.getSheetByName("GoogleGroupList");
  const googleGroupAddresses = googleGroupListSheet
    .getRange(1, 1, googleGroupListSheet.getLastRow())
    .getValues()
    .flat();

  const resultSheetName =
    Utilities.formatDate(new Date(), "JST", "yyyyMMddHHmmss") + "_result";
  const resultSheet = spreadsheet.insertSheet(resultSheetName);

  // ヘッダー設定
  let header = ["email"];

  const limits = 20; // メール取得件数
  for (let i = 0; i < limits; i++) {
    const idx = i + 1;
    header.push(
      `直近${idx}_受信日`,
      `直近${idx}_From`,
      `直近${idx}_To`,
      `直近${idx}_Cc`,
      `直近${idx}_件名`
    );
  }
  resultSheet.appendRow(header);

  googleGroupAddresses.forEach((googleGroupAddress) => {
    let row = [];
    if (googleGroupAddress) {
      row.push(googleGroupAddress);

      const threads = GmailApp.search(
        `(to:(${googleGroupAddress}) OR cc:(${googleGroupAddress}) OR bcc:(${googleGroupAddress})) -filename:vcs -filename:ics -replyto:google-workspace-alerts-noreply@google.com`,
        0,
        limits
      );

      threads.forEach((thread) => {
        const messages = thread.getMessages();
        const lastMessage = messages[messages.length - 1];

        // メールの情報を取得
        const dateReceived = lastMessage.getDate();
        const from = lastMessage.getFrom();
        const to = lastMessage.getTo();
        const cc = lastMessage.getCc();
        const subject = lastMessage.getSubject();

        // 取得した情報をスプレッドシートに記入
        row.push(dateReceived, from, to, cc, subject);
      });
    }
    resultSheet.appendRow(row);
  });
}

1.3. Google カレンダー予定

  • アプリ管理権限付与が付与されたまっさらな Google アカウントを用意してください
    • まっさらな Google アカウントに Google グループを順次割り当て、カレンダーに反映された予定を取得することで、Google グループに紐づいた予定を取得します
  • Google スプレッドシートにシート「GoogleGroupList」を作成し、A 列に取得したい Google グループのアドレスを記入してください
  • サービス AdminDirectory を追加してください
function getGoogleCalenderEvents() {
  const plainAccount = "plain_account@example.com"; // 用意したまっさらなGoogleアカウントを設定してください

  const spreadsheet = SpreadsheetApp.getActiveSpreadsheet();

  // 対象Googleグループアドレスを取得
  const googleGroupListSheet = spreadsheet.getSheetByName("GoogleGroupList");
  const googleGroupAddresses = googleGroupListSheet
    .getRange(1, 1, googleGroupListSheet.getLastRow())
    .getValues()
    .flat();

  // 結果シート(Googleカレンダー予定記載)を生成
  const resultSheetName =
    Utilities.formatDate(new Date(), "JST", "yyyyMMddHHmmss") +
    "_GoogleCalendarEvents";
  const resultSheet = spreadsheet.insertSheet(resultSheetName);

  const header = [
    "Google Group Address",
    "Subject",
    "Start Date",
    "Start Time",
    "End Date",
    "End Time",
    "All Day Event",
    "Description",
    "Location",
    "Visibility",
    "Creators",
  ];
  resultSheet.appendRow(header);

  googleGroupAddresses.forEach((googleGroupAddress) => {
    // 対象のGoogleグループに共有確認アカウントを追加
    addGoogleGroupMember(plainAccount, googleGroupAddress);

    // 対象のGoogleグループに共有確認アカウントを追加してから反映までにタイムラグがあるため30秒sleep
    Utilities.sleep(30000);

    // 対象のグループの予定を取得
    let calendarEvents = getCalenderEvent();

    // 予定を取得できなかったら30秒後再実行
    if (calendarEvents.length === 0) {
      Utilities.sleep(30000);
      calendarEvents = getCalenderEvent();
    }

    // 予定の先頭に対象のGoogleグループアドレスを追加
    const calendarEventsWithGoogleGroupAddress = calendarEvents.map(
      (calendarEvent) => [googleGroupAddress, ...calendarEvent]
    );

    // 結果シートに取得した予定を記載
    resultSheet
      .getRange(
        resultSheet.getLastRow() + 1,
        1,
        calendarEventsWithGoogleGroupAddress.length,
        calendarEventsWithGoogleGroupAddress[0].length
      )
      .setValues(calendarEventsWithGoogleGroupAddress);

    // 対象のGoogleグループに共有確認アカウントを削除
    removeGoogleGroupMember(plainAccount, googleGroupAddress);

    // 対象のGoogleグループから共有確認アカウントを削除してから反映までにタイムラグがあるため30秒sleep
    Utilities.sleep(30000);
  });
}

// Googleグループにメンバーを追加する関数
function addGoogleGroupMember(userEmail, groupEmail) {
  console.log(`User ${userEmail} added as a member of group ${groupEmail}.`);
  const member = { email: userEmail, role: "MEMBER" };
  AdminDirectory.Members.insert(member, groupEmail);
}

// Googleグループからメンバーを削除する関数
function removeGoogleGroupMember(userEmail, groupEmail) {
  console.log(`User ${userEmail} removed from group ${groupEmail}.`);
  AdminDirectory.Members.remove(groupEmail, userEmail);
}

// Googleカレンダーの予定を取得する関数
function getCalenderEvent() {
  const myCalendars = CalendarApp.getAllOwnedCalendars(); //自身のGoogleカレンダーを取得する

  //Googleカレンダーから取得するイベントの開始日(30日前)を設定する
  let startDate = new Date();
  startDate.setDate(startDate.getDate() - 30);
  //Googleカレンダーから取得するイベントの終了日(今日)を設定する
  const endDate = new Date();

  //開始日~終了日に存在するGoogleカレンダーのイベントを取得する
  const myEvents = myCalendars.reduce((acc, myCalendar) => {
    return acc.concat(myCalendar.getEvents(startDate, endDate));
  }, []);

  // 取得したイベントから必要項目を抽出した配列を生成する
  return myEvents.map((myEvent) => {
    const subject = myEvent.getTitle();

    const startTimeObject = myEvent.getStartTime();
    const startDate = Utilities.formatDate(
      startTimeObject,
      "JST",
      "yyyy/MM/dd"
    );
    const startTime = Utilities.formatDate(startTimeObject, "JST", "hh:mm");

    const endTimeObject = myEvent.getEndTime();
    const endDate = Utilities.formatDate(endTimeObject, "JST", "yyyy/MM/dd");
    const endTime = Utilities.formatDate(endTimeObject, "JST", "hh:mm");

    const allDatEvent = myEvent.isAllDayEvent();
    const description = myEvent.getDescription();
    const location = myEvent.getLocation();
    const visibility = myEvent.getVisibility();
    const creators = myEvent.getCreators().toString();

    return [
      subject,
      startDate,
      startTime,
      endDate,
      endTime,
      allDatEvent,
      description,
      location,
      visibility,
      creators,
    ];
  });
}

1.4. Google ドライブ共有設定

  • Google スプレッドシートにシート「GoogleGroupList」を作成し、A 列に取得したい Google グループのアドレスを記入してください
  • サービス Drive を追加してください
function getGoogleSharedDriveMembers() {
  const spreadsheet = SpreadsheetApp.getActiveSpreadsheet();

  // 対象Googleグループアドレスを取得
  const googleGroupListSheet = spreadsheet.getSheetByName("GoogleGroupList");
  const googleGroupAddresses = googleGroupListSheet
    .getRange(1, 1, googleGroupListSheet.getLastRow())
    .getValues()
    .flat();

  // 結果シート(Googleカレンダー予定記載)を生成
  const resultSheetName =
    Utilities.formatDate(new Date(), "JST", "yyyyMMddHHmmss") +
    "_GoogleSharedDriveMembers";
  const resultSheet = spreadsheet.insertSheet(resultSheetName);

  let values = [];
  const header = ["Google Group Address", "Google Shared Drive Name", "Role"];
  values.push(header);

  // 共有ドライブ一覧を取得
  const googleSharedDrivesList = getGoogleSharedDrivesList();

  googleSharedDrivesList.forEach((googleSharedDrive) => {
    const googleSharedDriveID = googleSharedDrive["id"];
    const googleSharedDriveName = googleSharedDrive["name"];

    // 共有ドライブのメンバーを取得
    const members = getMembers(googleSharedDriveID, googleSharedDriveName);

    // 共有ドライブのメンバーから対象Googleグループアドレスのメンバーを抽出
    const googleGroupMembers = members.filter((member) => {
      return googleGroupAddresses.includes(member["emailAddress"]);
    });

    googleGroupMembers.forEach((googleGroupMember) => {
      const googleGroupAddress = googleGroupMember["emailAddress"];
      const role = googleGroupMember["role"];
      values.push([googleGroupAddress, googleSharedDriveName, role]);
    });
  });

  resultSheet.getRange(1, 1, values.length, header.length).setValues(values);
}

// 共有ドライブ一覧を取得する関数
function getGoogleSharedDrivesList() {
  let nextPageToken;
  let googleSharedDrivesList = [];

  do {
    const drives = Drive.Drives.list({
      maxResults: 100,
      useDomainAdminAccess: true,
      pageToken: nextPageToken,
    });
    googleSharedDrivesList = googleSharedDrivesList.concat(drives.items);
    nextPageToken = drives.nextPageToken;
  } while (nextPageToken);

  return googleSharedDrivesList;
}

// 共有ドライブのメンバーを取得する関数
function getMembers(driveId, driveName) {
  let nextPageToken;
  let members = [];

  try {
    do {
      const drivePermissions = Drive.Permissions.list(driveId, {
        pageToken: nextPageToken,
        supportsAllDrives: true,
      });

      members = members.concat(drivePermissions.items);
      nextPageToken = drivePermissions.nextPageToken;
    } while (nextPageToken);
  } catch (err) {
    console.warn(
      `共有ドライブ "${driveId}: ${driveName}" のメンバの取得に失敗しました: ${err}`
    );
  }

  return members;
}

次回へ続く

本記事で対応方針1〜3 を書く予定でしたが、2,3 については後日改めて別記事にて執筆したいと思います。

  • 次回「2024 年(Google グループ)削除の旅」
  • 最終回「時計じかけの Google グループ棚卸し」

MICIN ではメンバーを大募集しています。
「とりあえず話を聞いてみたい」でも大歓迎ですので、お気軽にご応募ください!
MICIN 採用ページ:https://recruit.micin.jp/

株式会社MICIN

Discussion