📝

IAM のカスタマー管理ポリシーの削除を自動化する Chrome 拡張機能を作ってみた

に公開

IAM のカスタマー管理ポリシーをコンソールから削除する場合、以下の手順を繰り返す必要があります。

  1. 削除対象の IAM ポリシーのラジオボタンをクリック
  2. 削除ボタンをクリック
  3. ポップアップで削除対象の IAM ポリシー名を入力
  4. ポップアップで削除ボタンをクリック


この操作を自動化してみました。

1. 拡張機能のフォルダとファイル作成

ローカル PC 内に任意のフォルダを作成し、以下のファイルを作成します。

  • manifest.json
  • content.js
  • popup.js
  • popup.html

今回は以下のようなフォルダ構成にしました。

iam-policy-bulk-delete/
├── manifest.json
├── content.js
├── popup.js
├── popup.html

2. ファイルの編集

各種ファイルの内容は以下の通りです。

manifest.json
manifest.json
{
  "manifest_version": 3,
  "name": "IAM Policy Bulk Delete",
  "version": "1.0",
  "description": "AWS IAMポリシーを一括削除するための拡張機能",
  "permissions": ["activeTab", "scripting", "storage"],
  "host_permissions": ["https://*.console.aws.amazon.com/*"],
  "action": {
    "default_popup": "popup.html",
    "default_title": "IAM Policy Bulk Delete"
  },
  "content_scripts": [
    {
      "matches": ["https://*.console.aws.amazon.com/iam*"],
      "js": ["content.js"],
      "run_at": "document_idle"
    }
  ]
}

content.js
content.js
// 表示モード: 'radio' または 'checkbox'
let displayMode = "radio";

// ストレージから表示モードを読み込む
chrome.storage.local.get(["displayMode"], (result) => {
  displayMode = result.displayMode || "radio";
  setTimeout(() => {
    updateDisplay();
  }, 500);
});

// ポップアップからのメッセージを受信
chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
  if (request.action === "changeDisplayMode") {
    displayMode = request.mode;
    if (displayMode === "checkbox") {
      convertRadioToCheckbox();
    }
    updateDisplay();
    sendResponse({ success: true, mode: displayMode });
  }
  return true;
});

// ページが完全に読み込まれるまで待機
function waitForPageLoad() {
  if (!location.href.includes("#/policies")) {
    return;
  }

  let attempts = 0;
  const maxAttempts = 10;

  const checkInterval = setInterval(() => {
    attempts++;
    const radioButtons = document.querySelectorAll('input[type="radio"]');

    if (radioButtons.length > 0) {
      clearInterval(checkInterval);
      init();
    } else if (attempts >= maxAttempts) {
      clearInterval(checkInterval);
    }
  }, 1000);
}

function init() {
  convertRadioToCheckbox();
  addBulkDeleteButton();
  updateDisplay();
}

// 表示を更新
function updateDisplay() {
  const rows = document.querySelectorAll('tr[data-selection-item="item"]');

  rows.forEach((row) => {
    const firstCell = row.querySelector("td");
    if (!firstCell) return;

    const checkbox = firstCell.querySelector(".bulk-delete-checkbox");
    const radioLabel = firstCell.querySelector("label");

    if (displayMode === "checkbox") {
      if (checkbox) checkbox.style.display = "inline-block";
      if (radioLabel) radioLabel.style.display = "none";
    } else {
      if (checkbox) checkbox.style.display = "none";
      if (radioLabel) {
        radioLabel.style.display = "inline-block";
        radioLabel.style.pointerEvents = "auto";
      }
    }
  });
}

// 各ポリシー行にチェックボックスを追加
function convertRadioToCheckbox() {
  const rows = document.querySelectorAll('tr[data-selection-item="item"]');

  rows.forEach((row) => {
    if (row.querySelector(".bulk-delete-checkbox")) {
      return;
    }

    const firstCell = row.querySelector("td");
    if (!firstCell) return;

    const radioLabel = firstCell.querySelector("label");
    if (radioLabel) {
      radioLabel.style.pointerEvents = "none";
    }

    const checkbox = document.createElement("input");
    checkbox.type = "checkbox";
    checkbox.className = "bulk-delete-checkbox";
    checkbox.style.cssText = `
      margin-right: 10px;
      width: 18px;
      height: 18px;
      cursor: pointer;
      vertical-align: middle;
      position: relative;
      z-index: 1000;
    `;

    const cellContent = firstCell.querySelector(
      ".awsui_body-cell-content_c6tup_1wfrk_160"
    );
    if (cellContent) {
      cellContent.insertBefore(checkbox, cellContent.firstChild);
    }
  });
}

// 一括削除ボタンを追加
function addBulkDeleteButton() {
  if (document.querySelector(".bulk-delete-action-btn")) {
    return;
  }

  const originalDeleteBtn = document.querySelector(
    'button[data-testid="policies-delete-btn"]'
  );
  if (!originalDeleteBtn) {
    return;
  }

  const bulkDeleteBtn = document.createElement("button");
  bulkDeleteBtn.className = "bulk-delete-action-btn";
  bulkDeleteBtn.textContent = "選択したポリシーを一括削除";
  bulkDeleteBtn.style.cssText = `
    margin-left: 10px;
    padding: 8px 16px;
    background-color: #d13212;
    color: white;
    border: none;
    border-radius: 4px;
    cursor: pointer;
    font-size: 14px;
    font-weight: 500;
  `;

  bulkDeleteBtn.addEventListener("mouseenter", () => {
    bulkDeleteBtn.style.backgroundColor = "#a32810";
  });
  bulkDeleteBtn.addEventListener("mouseleave", () => {
    bulkDeleteBtn.style.backgroundColor = "#d13212";
  });

  bulkDeleteBtn.addEventListener("click", () => {
    handleBulkDelete();
  });

  originalDeleteBtn.parentElement.appendChild(bulkDeleteBtn);
}

// 一括削除処理
async function handleBulkDelete() {
  const checkedBoxes = document.querySelectorAll(
    ".bulk-delete-checkbox:checked"
  );

  if (checkedBoxes.length === 0) {
    alert("削除するポリシーを選択してください");
    return;
  }

  const policies = [];
  checkedBoxes.forEach((checkbox) => {
    const row = checkbox.closest("tr");
    const policyLink = row.querySelector("a");
    const radioButton = row.querySelector('input[type="radio"]');

    if (policyLink && radioButton) {
      policies.push({
        name: policyLink.textContent.trim(),
        row: row,
        radio: radioButton,
      });
    }
  });

  for (let i = 0; i < policies.length; i++) {
    const policy = policies[i];

    try {
      await deletePolicy(policy);
    } catch (error) {
      // エラーは無視して続行
    }

    await sleep(2000);
  }
}

// 個別のポリシーを削除
async function deletePolicy(policy) {
  return new Promise(async (resolve, reject) => {
    try {
      policy.radio.click();
      await sleep(1000);

      let deleteBtn = null;
      let attempts = 0;
      const maxAttempts = 20;

      while (attempts < maxAttempts) {
        deleteBtn = document.querySelector(
          'button[data-testid="policies-delete-btn"]:not([disabled])'
        );
        if (deleteBtn) break;
        await sleep(500);
        attempts++;
      }

      if (!deleteBtn) {
        reject(new Error("削除ボタンタイムアウト"));
        return;
      }

      deleteBtn.click();
      await sleep(1000);

      const inputField = document.querySelector(
        'input[type="text"][placeholder]'
      );
      if (!inputField) {
        reject(new Error("入力フィールドなし"));
        return;
      }

      inputField.value = policy.name;
      const nativeInputValueSetter = Object.getOwnPropertyDescriptor(
        window.HTMLInputElement.prototype,
        "value"
      ).set;
      nativeInputValueSetter.call(inputField, policy.name);
      inputField.dispatchEvent(new Event("input", { bubbles: true }));
      inputField.dispatchEvent(new Event("change", { bubbles: true }));

      await sleep(1000);

      let confirmBtn = null;
      attempts = 0;

      while (attempts < maxAttempts) {
        confirmBtn = document.querySelector(
          'button[data-testid="policies-confirm-delete-btn"]:not([disabled])'
        );
        if (confirmBtn) break;
        await sleep(300);
        attempts++;
      }

      if (!confirmBtn) {
        reject(new Error("確認ボタンタイムアウト"));
        return;
      }

      confirmBtn.click();
      await sleep(2000);

      attempts = 0;
      while (attempts < 10) {
        const dialog = document.querySelector(
          'button[data-testid="policies-confirm-delete-btn"]'
        );
        if (!dialog) break;
        await sleep(500);
        attempts++;
      }

      resolve();
    } catch (error) {
      reject(error);
    }
  });
}

// スリープ関数
function sleep(ms) {
  return new Promise((resolve) => setTimeout(resolve, ms));
}

// ページ読み込み後に実行
if (document.readyState === "loading") {
  document.addEventListener("DOMContentLoaded", waitForPageLoad);
} else {
  waitForPageLoad();
}

// ページ遷移やフィルタリング時に再度チェックボックスを追加
const observer = new MutationObserver((mutations) => {
  const rows = document.querySelectorAll('tr[data-selection-item="item"]');
  const checkboxCount = document.querySelectorAll(
    ".bulk-delete-checkbox"
  ).length;

  if (rows.length > 0 && rows.length !== checkboxCount) {
    convertRadioToCheckbox();
    if (!document.querySelector(".bulk-delete-action-btn")) {
      addBulkDeleteButton();
    }
    updateDisplay();
  }
});

setTimeout(() => {
  const tableContainer = document.querySelector("table");
  if (tableContainer) {
    observer.observe(tableContainer, {
      childList: true,
      subtree: true,
    });
  }
}, 2000);

// URL変更を検知(SPA対応)
let lastUrl = location.href;

new MutationObserver(() => {
  const currentUrl = location.href;
  if (currentUrl !== lastUrl) {
    lastUrl = currentUrl;

    if (currentUrl.includes("#/policies")) {
      document
        .querySelectorAll(".bulk-delete-checkbox")
        .forEach((el) => el.remove());
      const existingBtn = document.querySelector(".bulk-delete-action-btn");
      if (existingBtn) existingBtn.remove();

      setTimeout(() => {
        waitForPageLoad();
      }, 1000);
    }
  }
}).observe(document, { subtree: true, childList: true });
popup.js
popup.js
// 現在の表示モードを取得して表示
chrome.storage.local.get(["displayMode"], (result) => {
  const mode = result.displayMode || "radio"; // デフォルトを 'radio' に
  updateStatus(mode);
  updateButtonStyles(mode);
});

// チェックボックス表示ボタン
document.getElementById("showCheckbox").addEventListener("click", () => {
  setDisplayMode("checkbox");
});

// ラジオボタン表示ボタン
document.getElementById("showRadio").addEventListener("click", () => {
  setDisplayMode("radio");
});

// 表示モードを設定
function setDisplayMode(mode) {
  // ストレージに保存
  chrome.storage.local.set({ displayMode: mode }, () => {
    console.log(`表示モードを ${mode} に設定しました`);
    updateStatus(mode);
    updateButtonStyles(mode);

    // アクティブなタブにメッセージを送信
    chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => {
      if (tabs[0]) {
        chrome.tabs.sendMessage(
          tabs[0].id,
          {
            action: "changeDisplayMode",
            mode: mode,
          },
          (response) => {
            if (chrome.runtime.lastError) {
              console.log(
                "メッセージ送信エラー:",
                chrome.runtime.lastError.message
              );
            } else {
              console.log("メッセージ送信成功:", response);
            }
          }
        );
      }
    });
  });
}

// ステータス表示を更新
function updateStatus(mode) {
  const statusText =
    mode === "checkbox" ? "チェックボックス表示" : "ラジオボタン表示";
  document.getElementById("status").textContent = `現在: ${statusText}`;
}

// ボタンのスタイルを更新
function updateButtonStyles(mode) {
  const checkboxBtn = document.getElementById("showCheckbox");
  const radioBtn = document.getElementById("showRadio");

  if (mode === "checkbox") {
    checkboxBtn.classList.add("active");
    radioBtn.classList.remove("active");
  } else {
    radioBtn.classList.add("active");
    checkboxBtn.classList.remove("active");
  }
}
popup.html
popup.html
<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <style>
    body {
      width: 250px;
      padding: 15px;
      font-family: Arial, sans-serif;
    }
    h3 {
      margin: 0 0 15px 0;
      font-size: 16px;
      color: #232f3e;
    }
    .button-group {
      display: flex;
      flex-direction: column;
      gap: 10px;
    }
    button {
      padding: 10px;
      border: 1px solid #d5dbdb;
      border-radius: 4px;
      background-color: #fff;
      cursor: pointer;
      font-size: 14px;
      transition: background-color 0.2s;
    }
    button:hover {
      background-color: #f0f0f0;
    }
    button.active {
      background-color: #0073bb;
      color: white;
      border-color: #0073bb;
    }
    .status {
      margin-top: 15px;
      padding: 10px;
      background-color: #f0f8ff;
      border-radius: 4px;
      font-size: 12px;
      color: #0073bb;
    }
  </style>
</head>
<body>
  <h3>IAM Policy Bulk Delete</h3>
  <div class="button-group">
    <button id="showCheckbox">☑ チェックボックス表示</button>
    <button id="showRadio">◉ ラジオボタン表示</button>
  </div>
  <div class="status" id="status">現在: チェックボックス表示</div>
  <script src="popup.js"></script>
</body>
</html>

3. Chrome で読み込む

手順 1,2 完了後、Chrome の拡張機能のページから手順 1 で作成したフォルダを読み込みます。

4. 動作確認

IAM コンソールからカスタマー管理ポリシーを表示します。
今回はテスト用に以下のコマンドで 10 個のカスタマー管理ポリシーを作成しておきました。

$ for i in {1..10}; do POLICY_NAME="TestBulkDeletePolicy$(date +%s)${i}"; aws iam create-policy --policy-name "$POLICY_NAME" --policy-document '{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Action":"s3:GetObject","Resource":"arn:aws:s3:::test-bucket/*"}]}' --description "テスト用ポリシー" && echo "✓ [$i/10] $POLICY_NAME 作成完了" || echo "✗ [$i/10] 失敗"; sleep 0.5; done

拡張機能をクリックして「チェックボックスを表示」に切り替えます。

ポリシー名の左側にあるチェックボックスにチェックを入れて「選択したポリシーを一括削除」をクリックします。
なお、確認ポップアップなどは表示せずに削除しますのでご注意ください。

また、AWS 公式ツールではないので拡張機能の使用は自己責任でお願いします。

まとめ

今回は IAM のカスタマー管理ポリシーの削除を自動化する Chrome 拡張機能を作ってみました。
どなたかの参考になれば幸いです。

Discussion