Claude Media
Claude CodeでGoogleドライブの外部共有ファイルを棚卸しする手順

Claude CodeでGoogleドライブの外部共有ファイルを棚卸しする手順

Drive APIのfiles.listとpermissions.listで「リンクを知っている全員」や社外への共有を抜き出し、Claude Codeでオーナー別の見直し依頼表にする手順です。

Claude CodeでGoogleドライブの外部共有ファイルを棚卸しする手順

Googleドライブの外部共有は、Drive APIの files.list で共有済みのファイルを集め、permissions.list で共有先を1件ずつ開くと洗い出せます。「リンクを知っている全員」と社外ドメインへの共有だけをCSVに残し、Claude Codeにオーナー別の見直し依頼表を作らせる流れです。

取得はスクリプトが担い、Claude Codeは読み取り専用で並べ替えと文面づくりに専念させます。共有を外す操作は、この手順に含めません。

外部共有はPermissionのtypeで見分ける

ファイルの共有先は、Permissionリソースに1件ずつ入っています。外部共有の判定に使うのは type です。

type共有先外部共有の見方
anyone共有先リンクを知っている全員、または全員外部共有の見方常に対象。allowFileDiscovery が true なら検索にも出る
domain共有先特定ドメインの全員外部共有の見方domain が自社ドメインでなければ対象
user共有先個人のGoogleアカウント外部共有の見方emailAddress のドメインが自社外なら対象
group共有先Googleグループ外部共有の見方emailAddress のドメインが自社外なら対象

role は付与された権限で、owner organizer fileOrganizer writer commenter reader が入ります。同じ外部共有でも、reader と writer では見直しの優先度が違うので、CSVに残します。

allowFileDiscovery は、domain と anyone にだけ意味を持つ項目です。true だと、リンクを知らない相手の検索結果にもファイルが出ます。リンクを知っている人だけが開ける状態と、検索で見つかる状態は別物なので、列を分けて持ちます。

expirationTime にも癖があります。有効期限を設定できるのは user と group の権限だけです。anyone の共有には期限を付けられないため、期限切れを待つ運用では、リンク公開が残り続けます。

取得は2段になる

この記事では、次の2段で進めます。ファイルの一覧と、ファイルごとの共有先の一覧を、別々のメソッドで取ります。

  1. files.list で、自分が所有する共有済みファイルを集める
  2. 共有済みのファイルごとに permissions.list を呼んで、共有先を取る

1段目は、q に 'me' in owners and trashed=false を渡します。files.list は削除済みのファイルも既定で返すため、trashed=false を付けます。pageSize の上限は1000件で、続きは nextPageToken で取ります。

返ってきたFileリソースの shared は、ファイルが共有されたかどうかを示す出力専用の項目です。これが true のものだけを2段目に回すと、呼び出し回数を減らせます。

2段目の permissions.list は、fileId を渡すと権限の一覧を返します。pageSize は最大100件で、これを超える値は100に切り詰められます。nextPageToken が返ったときは、一覧が途中なので次のページも取ります。

検索クエリで先に絞る方法

リンク公開だけを先に知りたいなら、q の visibility が使えます。値は anyoneCanFind anyoneWithLink domainCanFind domainWithLink limited の5つで、単一引用符で囲みます。

'me' in owners and trashed=false and visibility='anyoneWithLink'

この5値は公開範囲のレベルを表すだけで、誰に共有したかは分かりません。社外の特定の個人やドメインへの共有を見るには、visibility の絞り込みに頼らず、共有済みのファイルに permissions.list を当てて emailAddress と domain を読みます。

fieldsで必要な項目を指定する

permissions.list は、指定しないと一部の項目しか返しません。常に返るのは kind id type role の4つで、emailAddress や domain は含まれません。fields パラメーターで、使う項目を明示します。

読み取り専用のスクリプトでCSVを作る

必要なスコープは、files.list と permissions.list のどちらも drive.metadata.readonly で足ります。メタデータの読み取りだけを許すスコープなので、このスクリプトから共有設定を変える経路は残りません。

次は、OAuthクライアントの credentials.json を用意した前提のPythonの一例です。

pip install google-api-python-client google-auth-oauthlib
python audit_sharing.py > external_shares.csv
# audit_sharing.py
import csv, sys
from google_auth_oauthlib.flow import InstalledAppFlow
from googleapiclient.discovery import build
 
OWN_DOMAIN = "example.com"  # 自社ドメイン
SCOPES = ["https://www.googleapis.com/auth/"
          "drive.metadata.readonly"]
creds = InstalledAppFlow.from_client_secrets_file(
    "credentials.json", SCOPES).run_local_server(port=0)
drive = build("drive", "v3", credentials=creds)
 
def pages(call, key, **kw):
    token = None
    while True:
        res = call(pageToken=token, **kw).execute()
        yield from res.get(key, [])
        token = res.get("nextPageToken")
        if not token:
            break
 
def external_reason(p):
    t = p["type"]
    if t == "anyone":
        return "リンク公開"
    if t == "domain" and p.get("domain") != OWN_DOMAIN:
        return "社外ドメイン"
    if t in ("user", "group"):
        mail = p.get("emailAddress", "")
        if not mail.endswith("@" + OWN_DOMAIN):
            return "社外の個人・グループ"
    return None
 
COLS = ["fileId", "permissionId", "owner", "fileName",
        "webViewLink", "type", "target",
        "role", "allowFileDiscovery", "expirationTime",
        "inherited", "reason"]
w = csv.writer(sys.stdout)
w.writerow(COLS)
 
files = pages(
    drive.files().list, "files",
    q="'me' in owners and trashed=false",
    fields=("nextPageToken,files(id,name,webViewLink,"
            "shared,owners(emailAddress))"),
    pageSize=1000)
for f in files:
    if not f.get("shared"):
        continue
    owner = f["owners"][0]["emailAddress"]
    perms = list(pages(
        drive.permissions().list, "permissions",
        fileId=f["id"], pageSize=100,
        fields=("nextPageToken,permissions(id,type,role,"
                "emailAddress,domain,allowFileDiscovery,"
                "expirationTime,permissionDetails)")))
    if not perms:
        w.writerow([f["id"], "", owner, f["name"],
                    f["webViewLink"],
                    "", "", "", "", "", "", "権限一覧なし"])
        continue
    for p in perms:
        why = external_reason(p)
        if not why or p["role"] == "owner":
            continue
        det = p.get("permissionDetails")
        inh = any(d.get("inherited") for d in det) if det else ""
        w.writerow([f["id"], p["id"], owner, f["name"],
                    f["webViewLink"], p["type"],
                    p.get("emailAddress") or p.get("domain", ""),
                    p["role"], p.get("allowFileDiscovery", ""),
                    p.get("expirationTime", ""), inh, why])

このスクリプトが返すのは、実行したユーザー自身が所有するファイルだけです。複数のオーナー分をまとめて依頼表にするときは、各オーナーが自分のアカウントで実行し、できたCSVを見出し行を1つにして結合します。

# 2人目以降は見出し行を飛ばして連結する
cp tanaka.csv external_shares.csv
tail -n +2 suzuki.csv >> external_shares.csv

結合後のCSVは、次のような形になります。値はすべて架空の例です。

fileId,permissionId,owner,fileName,webViewLink,type,target,role,allowFileDiscovery,expirationTime,inherited,reason
1AbC...,0123...,a.tanaka@example.com,見積書_A社.xlsx,https://drive.google.com/...,anyone,,reader,False,,False,リンク公開
1DeF...,0456...,a.tanaka@example.com,提案資料.pptx,https://drive.google.com/...,user,k.ito@partner.example.org,writer,,,False,社外の個人・グループ
1GhI...,0789...,b.suzuki@example.com,議事録.docx,https://drive.google.com/...,anyone,,writer,True,,True,リンク公開

3行目のように、anyone で writer、かつ allowFileDiscovery が True の行は、見直しの順番が真っ先に来ます。

Claude Codeにオーナー別の依頼表を作らせる

CSVを置いたフォルダーに CLAUDE.md を書き、並べ方と禁止事項を固定します。口頭の指示だと、実行のたびに基準がぶれるためです。

# 外部共有の棚卸し
 
- 読むのは external_shares.csv だけ。API 呼び出しや共有設定の
  変更はしない
- 優先度は次の順に高い
  1. type が anyone で allowFileDiscovery が True
  2. type が anyone で role が writer
  3. type が anyone
  4. role が writer の社外ドメイン・社外の個人
  5. それ以外
- reason が「権限一覧なし」の行は別枠に出す
- inherited が True の行は、ファイル単体ではなく
  上位フォルダーの共有を見直す必要があると注記する
- inherited が空欄の行は、継承かどうかを判定できていないと注記する
- 出力はオーナーごとに表を分け、列は
  ファイル名, リンク, 共有先, 権限, 優先度, 確認してほしいこと
- 「確認してほしいこと」は質問の形で書き、削除を命じない

実行は非対話モードです。claude -p で依頼を渡すと結果が標準出力に返り、--allowedTools で許可するツールを絞れます。ここでは読み取りだけを許可します。非対話実行の詳しい作法はclaude -pの使い方にまとめています。

claude -p "external_shares.csv を CLAUDE.md の基準で整理して。\
オーナーごとの見直し依頼表にして" \
  --allowedTools "Read" > review_requests.md

「確認してほしいこと」を質問の形にしておくのは、依頼表を受け取るオーナーの側に判断を残すためです。取引先との共同作業中のファイルは、外部共有が正しい状態かもしれません。棚卸しで決められるのは「見直す価値があるか」までで、外すかどうかはオーナーが決めます。

依頼表の1行は、たとえば次のような形になります。ファイル名と人名は架空の例です。

ファイル名共有先権限優先度確認してほしいこと
見積書_A社.xlsx共有先リンクを知っている全員権限閲覧優先度3確認してほしいことA社との取引は続いていますか。続いていれば、A社のアドレスへの個別共有に切り替えられますか
提案資料.pptx共有先k.ito@partner.example.org権限編集優先度4確認してほしいこと共同編集は今も続いていますか。閲覧だけで足りる相手ではありませんか

優先度は CLAUDE.md の番号をそのまま使います。依頼の文面は、受け取ったオーナーが「何を答えれば終わるか」が分かる形にします。

件数をスクリプト側の数字で検算する

出力された表は、そのまま信じず、件数をCSV側と照らします。次のワンライナーで、リンク公開の件数をオーナー別に数え直せます。

python - <<'EOF'
import csv, collections
c = collections.Counter(
    r["owner"] for r in csv.DictReader(open("external_shares.csv"))
    if r["type"] == "anyone")
for owner, n in c.most_common():
    print(n, owner)
EOF

依頼表に載ったリンク公開の件数が、オーナーごとにこの数字と一致するかを見ます。ずれたオーナーは、表の行が欠けたか、別の枠に回された可能性があります。

Claude Codeに渡すCSVの範囲を決める

CSVには、ファイル名、オーナーのメールアドレス、社外の共有先のメールアドレスが入ります。ファイル名に取引先名や案件名が含まれる組織は多く、この列だけでも社外に出したくない情報になりえます。

渡す範囲を狭める方法は、2つあります。

  • 依頼表づくりに不要な列を、スクリプトの段階で落とす。たとえば fileName を出さず、fileId と webViewLink だけで依頼表を作らせると、ファイル名はオーナーが開いて確かめることになる
  • 社外ドメインの個人アドレスは、target をドメイン部分だけに置き換えてから渡す。誰に共有されたかはオーナーが管理画面で確かめられる

どこまで落とすかは、組織の情報取り扱いの規程で決まります。Claude Codeに読ませる前に、データの持ち出し先として許される範囲かを確かめてください。

見直しが済んだ後は、同じスクリプトを回し直して、直したはずの行が消えているかを確かめます。親フォルダーから継承された権限は、子のファイル側では外せず、変更は親フォルダーで行います。permissionDetails の inherited は、公式の手順では共有ドライブ向けの項目として説明されています。マイドライブのファイルで値が入るかはページに記載がないので、inherited が空欄の行が残るなら、そのファイルの親フォルダーの共有設定を開いて確かめます。

前回の棚卸しとの差分を取る

一度きりで終わらせず、月に1回ほど同じスクリプトを回すと、新しく増えた外部共有だけを追えます。差分のキーには fileId と permissionId の組を使います。permissions.list の id は、共有先1件ごとに固有の識別子で、不透明な値として扱う決まりです。

claude -p "external_shares_prev.csv と external_shares.csv を\
 fileId と permissionId の組で比べて、今回新しく増えた行と、\
 消えた行を分けて出して" --allowedTools "Read" > diff.md

増えた行はオーナーへの追加の確認に回し、消えた行は「見直し済み」として前回の依頼表から外します。同じ件数に見えても、中身が入れ替わっている月は、この比較でしか見つかりません。

共有ドライブは別の呼び出しで取る

共有ドライブのファイルは、'me' in owners では拾えません。Fileリソースの shared と owners に値が入らないためです。

files.list には、共有ドライブを対象にするためのパラメーターがあります。corpora に drive を、driveId に共有ドライブのIDを渡します。supportsAllDrives と includeItemsFromAllDrives も合わせて指定します。効率の面では、allDrives より user か drive を選ぶことが勧められています。

permissions.list 側には supportsAllDrives があり、共有ドライブ上のファイルの権限を読むときに指定します。共有ドライブそのものの権限を管理者として読む場合に限り、useDomainAdminAccess が使えます。条件は、fileId が共有ドライブを指し、呼び出し側がそのドライブの属するドメインの管理者であることです。

共有ドライブでは、permissionDetails の inheritedFrom に、権限の継承元になったアイテムのIDが入ります。上位フォルダーを見直すときの手がかりになります。

取りこぼしを生む4つの条件

件数が合っていても、CSVに入らなかった共有はありえます。次の4つは、取得の段階で起きます。

  • 権限一覧が返らない: Fileリソースの permissions は、呼び出したユーザーがそのファイルを共有できるときにだけ取れます。共有する権限のないファイルは、shared が true でも一覧が空になりえます。スクリプトが「権限一覧なし」の行を残すのはこのためです
  • 共有ドライブのファイルが対象外: 上の節のとおり、別の呼び出しが要ります
  • 継承された共有が子では直せない: 見直し先は親フォルダーです。inherited が空欄の行の扱いは「Claude Codeに渡すCSVの範囲を決める」の節の末尾にあります
  • 呼び出しユーザー1人分しか取れない: files.list と permissions.list のページには、組織全体のファイルを1回で取る方法の記載がありません。オーナーごとに実行して結合する前提で進めます

まとめ

外部共有の棚卸しで効くのは、type と allowFileDiscovery と role の3列を持つCSVです。リンク公開の書き込み権限と、検索に出るリンク公開を先に片づけると、見直しの件数に対する効果が大きくなります。

未使用アカウントの側から見直す場合は、Google Workspaceの未使用アカウントをClaude Codeで洗い出す手順が補完になります。退職や異動で使われなくなったアカウントが、外部共有を持ったまま残っていないかを突き合わせられます。ブラウザー上で共有ファイルの整理を進める場合は、Claude in ChromeでGoogleドライブを整理する手順も使えます。

参照した一次ソース

この記事の記述は、次の一次ソースと照合しています。更新時に参照先が変わった場合は一覧も更新します。

  1. developers.google.com/workspace/drive/api/reference/rest/v3/permissions/list
  2. developers.google.com/workspace/drive/api/reference/rest/v3/permissions
  3. developers.google.com/workspace/drive/api/reference/rest/v3/files/list
  4. developers.google.com/workspace/drive/api/reference/rest/v3/files
  5. developers.google.com/workspace/drive/api/guides/ref-search-terms
  6. developers.google.com/workspace/drive/api/guides/manage-sharing
  7. code.claude.com/docs/en/headless
この記事を共有:XはてブLinkedIn