Graph APIでSharePointのフォルダやファイルを操作する(Python)

# はじめに
この記事では、Pythonを使用してMicrosoft Graph APIを通じてSharePointにアクセスし、フォルダやファイルを操作する方法を紹介します。
以下のような感じで説明していきます。
1. SharePointのサイトにアクセスする方法
1. 認証
1. SharePointのフォルダやファイルの操作
- msalとrequestsを使用しています。
- Office365-REST-Python-Clientを使用した場合の操作は余裕ができたら後日記事にしようかと思います。
# 1. SharePointのサイトにアクセス
SharePointへのアクセスですが大きく分けて以下のようなものがあります
- パスワード認証
- SharePointでのアプリ登録
- Azureでアプリ登録をしてGraph APIを使用する

皆さんの置かれている環境などで選択するのが良いと思いますが、この記事ではGraph APIを使用する方法で実装しています。

## Azureでのアプリ登録
こちらが今回の記事で使用している方法です。
まずは[ポータルサイト](https://portal.azure.com/)にログインします
### Microsoft Entra IDを選択
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/956246/cf30aa53-63cd-b05e-6ec1-9bb99efc0684.png)

### メニュー>管理>アプリの登録 を選択
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/956246/c7bcd60d-c0c5-e5fb-891c-c0025a58ceb6.png)

### 新規登録
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/956246/5975eb6c-faf3-f364-9628-4bfdb81ef973.png)

### アプリケーション開発の登録
任意の名前を入力して、アカウントの種類を選択します。
ここではシングルテナントを選択しています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/956246/186de460-ab97-e471-5e72-ca2db157b0b8.png)
画面の下にある[登録]ボタンを押下して作成します。

### シークレット作成
登録すると以下のように表示されます。
この中ではアプリケーション(クライアント)IDとディレクトリ(テナント)IDを使用しますので控えておきましょう
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/956246/a6708641-c31c-f83d-88d0-554d728f3e3b.png)

さらに証明書またはシークレットの追加リンクを開いて、新しいクライアントシークレットを作ります
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/956246/2e07c1fe-9493-812a-97c6-7004bd33f816.png)
説明と有効期限を設定して、追加します
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/956246/a6295d7f-c9e8-5e01-0277-ecde28c6c5e9.png)
説明を空欄のままにすると適当な名前が当てられるみたいですwww
ここで必ず「値」を控えておいてください。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/956246/44b654ac-51c1-c58b-c86e-454024a83edb.png)

### APIのアクセス許可
次にメニューからAPIのアクセス許可を選択
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/956246/18595acf-271f-8979-9a6f-169bac4f05e6.png)
アクセス許可の追加を選択
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/956246/78d7facc-b349-75b4-efe4-1c78ede64599.png)
Microsoft Graphを選択
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/956246/342a71c4-f7a6-e0ff-fa8d-2afa2d753ee1.png)
アプリケーションの許可を選択
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/956246/d8fe18d1-a3c4-6dd5-a413-614df6c317f6.png)
Sites.ReadWrite.Allを選択(権限をどれにするかはそれぞれで判断してください)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/956246/9d61c0f3-ccb3-97bc-5047-cf0e4e62ef4e.png)
もう一つアクセス許可を追加します
Files.ReadWrite.All
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/956246/344a3e32-1b3d-a8cd-0c2d-15557d703192.png)
最後に管理者の同意を与えてもらいます
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/956246/3c12dee9-9b89-daea-e937-da7c3e26cf9e.png)

これでSharePointにアクセスするための設定は完了です。
以下の3つの値を使用します。
- アプリケーション(クライアント)ID
- ディレクトリ(テナント)ID
- シークレット(値)

# 2. 認証
今回のサンプルで使用するのは以下の通りです。
msalで認証処理を行い、アクセストークンを取得します。
```py
import msal
import requests
from pathlib import Path
import json
from functools import cache
import pprint as pp
```

SharePointAccessClassクラスを作成しています。クラスにするかは皆さんにお任せします。
クライアントIDなどはjsonファイルにしていて、そこから読み込んでいます。
## msalでアクセストークンを取得
```py
class SharePointAccessClass:
    # 初期化
    def __init__(self, client_id, client_secret, tenant_id):
        """
        Initialize the SharePointAccessClass
        """
        self.client_id = client_id # アプリケーション(クライアント)ID
        self.client_secret = client_secret # シークレット(値)
        self.tenant_id = tenant_id # ディレクトリ(テナント)ID
        self.authority = "https://login.microsoftonline.com/" + tenant_id
        self.scope = ["https://graph.microsoft.com/.default"]
        self.access_token: None | str = None
        self.get_access_token()


    # Access Tokenを取得する
    def get_access_token(self):
        """
        Get the access token using the client_id, client_secret, and tenant_id
        """
        # Create a confidential client application using msal library
        """msalを使用してアクセストークンを取得します"""
        app = msal.ConfidentialClientApplication(
            self.client_id,
            authority=self.authority,
            client_credential=self.client_secret
        )
        result = app.acquire_token_for_client(scopes=self.scope)
        if "access_token" in result:
            # Save the access token
            self.access_token = result["access_token"]
        else:
            raise Exception("No access token available")
```

# 3. SharePointのフォルダやファイルの操作
## 汎用API用メソッド
アクセストークンを使用してGet, Put, Delete, Postメソッドを実装します。
```py
    # Graph APIを使用してデータを取得する汎用GETメソッド
    @cache
    def graph_api_get(self, endpoint: str) -> requests.models.Response | None:
        """
        Get data from Graph API using the endpoint
        """
        if self.access_token is not None:
            graph_data = requests.get(
                endpoint,
                headers={'Authorization': 'Bearer ' + self.access_token})
            return graph_data
        else:
            raise Exception("No access token available")


    # Graph APIを使用してデータを送信する汎用PUTメソッド
    def graph_api_put(self, endpoint: str, data) -> requests.models.Response | None:
        """
        Post data to Graph API using the endpoint
        """
        if self.access_token is not None:
            graph_data = requests.put(
                url=endpoint,
                headers={'Authorization': 'Bearer ' + self.access_token},
                data=data)
            return graph_data
        else:
            raise Exception("No access token available")


    # Graph APIを使用してデータを削除する汎用DELETEメソッド
    def graph_api_delete(self, endpoint: str) -> requests.models.Response | None:
        """
        Delete data from Graph API using the endpoint
        """
        if self.access_token is not None:
            graph_data = requests.delete(
                endpoint,
                headers={'Authorization': 'Bearer ' + self.access_token})
            return graph_data
        else:
            raise Exception("No access token available")

    # Graph APIを使用してデータを送信する汎用POSTメソッド
    def graph_api_post(self, endpoint: str, data) -> requests.models.Response | None:
        """
        Post data to Graph API using the endpoint
        """
        if self.access_token is not None:
            graph_data = requests.post(
                url=endpoint,
                headers={'Authorization': 'Bearer ' + self.access_token},
                json=data)  # Use json parameter instead of data for POST requests
            return graph_data
        else:
            raise Exception("No access token available")
```

## サイトアクセス、フォルダアクセス用コードの実装
SharePointの階層に対応して、指定フォルダ(ディレクトリ)を探すためのコードです
```py
    # サイト一覧を取得する
    def get_sites(self):
        """
        Get Sites in SharePoint
        """
        print("Get Sites in SharePoint")
        endpoints = self.graph_api_get("https://graph.microsoft.com/v1.0/sites")
        print(endpoints.json())
        return endpoints.json()


    # サイト名からサイトIDを取得する
    def get_site_id(self, site_name):
        """
        Get Site_id  using the site_name
        """
        print(f"Get Site_id using the site_name: {site_name}")
        sites = self.get_sites()
        for site in sites['value']:
            if site['name'] == site_name:
                print(f"site: {site}")
                return site['id']
        return None


    # サイトIDからサイトのフォルダを全て取得する
    def get_folders(self, site_id, folder_id='root'):
        print(f"Get Subfolders in a folder using the folder_id: {folder_id}")
        folders = self.graph_api_get(
            f'https://graph.microsoft.com/v1.0/sites/{site_id}/drive/items/{folder_id}/children')
        if folders is not None:
            return folders.json()
        else:
            return None


    # サイトIDからサイトのフォルダIdを取得する
    def get_folder_id(self, site_id, folder_name, folder_id='root'):
        folders = self.get_folders(site_id, folder_id)
        for folder in folders['value']:
            if folder_name == folder["name"]:
                return folder['id']
        return None


    # サイトIDからサイトのフォルダを取得する
    def get_folder(self, site_id, folder_name, folder_id='root'):
        subfolders = self.get_folders(site_id, folder_id)
        for folder in subfolders['value']:
            if folder_name == folder["name"]:
                return folder
        return None


    # 指定されたサイトIDのサイトから、指定されたディレクトリツリーの最下層のフォルダIDを取得する
    def get_folder_id_from_tree(self, site_id, sharepoint_directories, folder_id='root'):
        # 各ディレクトリを上から順に表示
        for directory in sharepoint_directories:
            print(f"folder_name:= {directory}")
            folder_id = self.get_folder_id(site_id, directory, folder_id)

        print(f"folder_id: {folder_id}")
        return folder_id
```

## SharePoint上のフォルダの作成
```py
    # SharePoint上のフォルダの作成
    def create_folder(self, target_site_name, sharepoint_directory, folder_name):
        """
        Create a folder in SharePoint using the target_site_name, sharepoint_directory, and folder_name
        """
        print("Creating folder...")
        # ターゲットサイトのIDを取得
        target_site_id = self.get_site_id(target_site_name)
        # フォルダIDを取得
        folder_id = self.get_folder_id_from_tree(target_site_id, sharepoint_directory, 'root')

        if folder_id:
            url = f'https://graph.microsoft.com/v1.0/sites/{target_site_id}/drive/items/{folder_id}/children'

            # フォルダを作成
            data = {
                "name": folder_name,
                "folder": {},
                "@microsoft.graph.conflictBehavior": "rename"
            }
            graph_data = self.graph_api_post(url, data).json()
            return graph_data
        else:
            return "Folder not found"
```

## SharePoint上のフォルダの削除
```py
    # SharePoint上のフォルダの削除
    def delete_folder(self, target_site_name, sharepoint_directory, folder_name):
        """
        Delete a folder in SharePoint using the target_site_name, sharepoint_directory, and folder_name
        """
        print("Deleting folder...")
        # ターゲットサイトのIDを取得
        target_site_id = self.get_site_id(target_site_name)
        # フォルダIDを取得
        folder_id = self.get_folder_id_from_tree(target_site_id, sharepoint_directory, 'root')

        if folder_id:
            # フォルダを削除するためのURLは、/contentではなく、/items/{item-id}の形式である必要があります
            url = f'https://graph.microsoft.com/v1.0/sites/{target_site_id}/drive/items/{folder_id}:/{folder_name}'

            # フォルダの削除
            graph_data = self.graph_api_delete(url)
            if graph_data.status_code == 204:
                print("Folder deleted successfully")
            else:
                print(f"Failed to delete folder: {graph_data.status_code}")
            return graph_data
        else:
            return "Folder not found"
```

## SharePoint上の指定フォルダ内の一覧取得
```py
    # SharePoint上の指定フォルダ内の一覧取得
    def get_items_in_the_folder(self, target_site_name, sharepoint_directory):
        """
        Get Files in SharePoint using the target_site_name, sharepoint_directory
        """
        print("Get Files in SharePoint")
        # ターゲットサイトのIDを取得
        target_site_id = self.get_site_id(target_site_name)
        # フォルダIDを取得
        folder_id = self.get_folder_id_from_tree(target_site_id, sharepoint_directory, 'root')

        if folder_id:
            items = self.graph_api_get(
                f'https://graph.microsoft.com/v1.0/sites/{target_site_id}/drive/items/{folder_id}/children')
            if items is not None:
                return items.json()
            else:
                return None
        else:
            return "Folder not found"
```

## SharePoint上の指定フォルダの詳細情報取得
```py
    # SharePoint上の指定フォルダの詳細情報取得
    def get_folder_details(self, target_site_name, sharepoint_directory):
        """
        Get Folder Details in SharePoint using the target_site_name, sharepoint_directory
        """
        print("Get Folder Details in SharePoint")
        # ターゲットサイトのIDを取得
        target_site_id = self.get_site_id(target_site_name)
        # フォルダIDを取得
        folder_id = self.get_folder_id_from_tree(target_site_id, sharepoint_directory, 'root')

        if folder_id:
            folder_details = self.graph_api_get(
                f'https://graph.microsoft.com/v1.0/sites/{target_site_id}/drive/items/{folder_id}')
            if folder_details is not None:
                return folder_details.json()
            else:
                return None
        else:
            return "Folder not found"
```

## ファイルのアップロード
```py
    # ファイルのアップロード
    def upload_file(self, target_site_name, sharepoint_directory, object_file_path):
        """
        Upload a file to SharePoint using the target_site_name, sharepoint_directory, and object_file_path
        """
        print("Uploading file...")

        # ターゲットサイトのIDを取得
        target_site_id = self.get_site_id(target_site_name)
        # フォルダIDを取得
        folder_id = self.get_folder_id_from_tree(target_site_id, sharepoint_directory, 'root')

        if folder_id:
            # アップロードURLを作成
            url = f'https://graph.microsoft.com/v1.0/sites/{target_site_id}/drive/items/{folder_id}:/{object_file_path.name}:/content'
            # ファイルをアップロード
            with open(object_file_path, 'rb') as f:
                graph_data = self.graph_api_put(url, f)

            # アップロード結果を返す
            return graph_data.json()
        else:
            return "Folder not found"
```

## ファイルのダウンロード
```py
    # SharePointのファイルのダウンロード
    def download_file(self, target_site_name, sharepoint_directory, object_file_name, download_dir):
        """
        Download a file from SharePoint using the target_site_name, sharepoint_directory, and object_file_path
        """
        print("Downloading file...")
        # ターゲットサイトのIDを取得
        target_site_id = self.get_site_id(target_site_name)
        # フォルダIDを取得
        folder_id = self.get_folder_id_from_tree(target_site_id, sharepoint_directory, 'root')

        if folder_id:
            # ダウンロードURLを作成
            url = f'https://graph.microsoft.com/v1.0/sites/{target_site_id}/drive/items/{folder_id}:/{object_file_name}:/content'

            # ファイルをダウンロード
            graph_data = self.graph_api_get(url)

            # ファイルを保存
            download_file_path = Path(download_dir).joinpath(object_file_name)
            if graph_data.status_code == 200 and graph_data.content:
                with open(download_file_path, 'wb') as f:
                    f.write(graph_data.content)
                return download_file_path
            else:
                return "File not found"
        else:
            return "Folder not found"
```

## ファイルの読み込み
```py
    def read_file(self, target_site_name, sharepoint_directory, object_file_name):
        """
        Read a file from SharePoint using the target_site_name, sharepoint_directory, and object_file_path
        """
        print("Reading file...")
        # ターゲットサイトのIDを取得
        target_site_id = self.get_site_id(target_site_name)
        # フォルダIDを取得
        folder_id = self.get_folder_id_from_tree(target_site_id, sharepoint_directory, 'root')

        if folder_id:
            url = f'https://graph.microsoft.com/v1.0/sites/{target_site_id}/drive/items/{folder_id}:/{object_file_name}:/content'

            # ファイルの読み込み
            graph_data = self.graph_api_get(url)

            # ファイルを保存
            if graph_data.status_code == 200 and graph_data.content:
                return graph_data.content
            else:
                return "File not found"
        else:
            return "Folder not found"
```

## ファイルの削除
```py
    # SharePoint上のファイルの削除
    def delete_file(self, target_site_name, sharepoint_directory, object_file_name):
        """
        Delete a file from SharePoint using the target_site_name, sharepoint_directory, and object_file_path
        """
        print("Deleting file...")
        # ターゲットサイトのIDを取得
        target_site_id = self.get_site_id(target_site_name)
        # フォルダIDを取得
        folder_id = self.get_folder_id_from_tree(target_site_id, sharepoint_directory, 'root')

        if folder_id:
            url = f'https://graph.microsoft.com/v1.0/sites/{target_site_id}/drive/items/{folder_id}:/{object_file_name}'

            # ファイルの削除
            graph_data = self.graph_api_delete(url)
            if graph_data.status_code == 204:
                print("File deleted successfully")
            else:
                print(f"Failed to delete file: {graph_data.status_code}")
            return graph_data
        else:
            return "Folder not found"
```

以上で**クラスのコード**は完成です。
あとはこれらを使用してアクセスします。

## jsonファイルから設定を読み込んでクラスインスタンスを作成
```py
# jsonファイルを読み込む関数
def read_ms_parameter():
    """
    .secretのms_parameter.jsonファイルを読み込む関数
    """
    with open('../.secret/ms_parameter.json') as f:
        parameter = json.load(f)
    return parameter


# SharePointAccessClassのインスタンスを取得する
def get_sharepoint():
    """
    SharePointAccessClassのインスタンスを取得する
    """
    ms_parameter = read_ms_parameter()
    client_id = ms_parameter['client_id']
    client_secret = ms_parameter['client_secret']
    tenant_id = ms_parameter['tenant_id']
    sp = SharePointAccessClass(client_id, client_secret, tenant_id)
    return sp

# 設定ファイルからSharePointのサイト名を取得
def get_sharepoint_site_name_from_param():
    """
    SharePointのサイト名を取得する
    """
    ms_parameter = read_ms_parameter()
    return ms_parameter['site_name']


# 設定ファイルからSharePointのフォルダ名を取得
def get_sharepoint_folder_name_from_param():
    """
    SharePointのroot内のフォルダ名を取得する
    """
    ms_parameter = read_ms_parameter()
    return ms_parameter['sharepoint_directory']
```

## 実行して確認してみる
```py
if __name__ == '__main__':
    sharepoint = get_sharepoint()

    # 設定ファイルからSharePointのサイト名とフォルダ名を取得
    target_site_name = get_sharepoint_site_name_from_param() 
    target_path = get_sharepoint_folder_name_from_param()  

    # スラッシュで分割してリストにする
    sharepoint_directories = target_path.split('/')

    # SharePoint上のフォルダの作成
    folder_name = 'test_folder'
    response_create_folder = sharepoint.create_folder(target_site_name, sharepoint_directories, folder_name)
    print(f"sharepoint.create_folder: {response_create_folder}")

    # SharePoint上のフォルダの削除
    delete_folder = 'test_folder'
    response_delete_folder = sharepoint.delete_folder(target_site_name, sharepoint_directories, delete_folder)
    print(f"sharepoint.delete_folder: {response_delete_folder}")

    # SharePoint上の指定フォルダ内の一覧取得
    view_folder_directories = 'ragSource'.split('/')
    response_get_view = sharepoint.get_items_in_the_folder(target_site_name, view_folder_directories)
    pp.pprint(f"sharepoint.get_files: {response_get_view}")

    # SharePoint上の指定フォルダ内の一覧取得
    view_folder_directories = 'ragSource'.split('/')
    response_get_view2 = sharepoint.get_items_in_the_folder(target_site_name, view_folder_directories)
    # 取得した一覧の中からフォルダのみを取得
    # jsonファイルの中にfolder属性が存在するかで判別する
    folders = [folder for folder in response_get_view2['value'] if 'folder' in folder]
    pp.pprint(f"sharepoint.get_folders: {folders}")

    # SharePoint上の指定フォルダの詳細情報取得
    view_folder_details = 'folderA'.split('/')
    response_get_folder_details = sharepoint.get_folder_details(target_site_name, view_folder_details)
    pp.pprint(f"sharepoint.get_folder_details: {response_get_folder_details}")

    # ファイルのアップロード
    doc_dir = '../sourceDir'
    file_name = 'test.txt'
    upload_object_path = Path(doc_dir).joinpath(file_name)
    if upload_object_path.exists():
        response = sharepoint.upload_file(target_site_name, sharepoint_directories, upload_object_path)
        print(f"sharepoint.upload_file: {response}")
    else:
        print('ファイルが存在しません')

    # ファイルのダウンロード
    download_dir = Path(doc_dir).joinpath('../downloadDir')
    download_file = 'test.txt'
    response_downloaded_object = sharepoint.download_file(target_site_name, sharepoint_directories, download_file, download_dir)
    print(f"sharepoint.download_file: {response_downloaded_object}")

    # ファイルの読み込み
    read_file = 'test.txt'
    response_read_object = sharepoint.read_file(target_site_name, sharepoint_directories, download_file)
    print(f"sharepoint.read_file: {response_read_object}")

    # ファイルの削除
    delete_file = 'test.txt'
    response_delete_object = sharepoint.delete_file(target_site_name, sharepoint_directories, delete_file)
    print(f"sharepoint.delete_file: {response_delete_object}")

    # SharePoint上の指定フォルダ内の一覧取得
    view_folder_directories = 'folderA'.split('/')
    response_get_view2 = sharepoint.get_items_in_the_folder(target_site_name, view_folder_directories)
    # 取得した一覧の中からファイルのみを取得
    # jsonファイルの中にfile属性が存在するかで判別する
    files = [folder for folder in response_get_view2['value'] if 'file' in folder]
    pp.pprint(f"sharepoint.get_folders: {files}")
```

SharePointへのアクセスが結構難易度が高いというか、ややこしいですね。
ドキュメント探ったりしながら構築したので、思ったよりも時間がかかりました。
同じように苦労されている方の手助けになれば幸いです。


# おまけ
SharePointでのアプリ登録にもついて触れておこうと思います。

## SharePoint上でのアプリ登録
※この登録は簡単に触れているだけなので動作確認してません。権限不足などあるかもです。この記事では後述の**Azureでのアプリの登録**で実行しています。

https://learn.microsoft.com/ja-jp/sharepoint/dev/sp-add-ins/register-sharepoint-add-ins

### アプリの登録:
https://(your-sharepoint-site)/_layouts/15/appregnew.aspx に移動します。
「クライアントID」と「クライアントシークレット」を生成し、その他の必要な情報を入力します。
生成された「クライアントID」と「クライアントシークレット」を保存します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/956246/832ef333-1725-243a-b0dc-cfd4aecb72e1.png)

:::note warn
2026/4/2から...とあり、この方法は使えなくなる可能性があるみたいなので注意が必要ですね
:::

### アプリ権限の付与:
次に、https://(your-sharepoint-site)/_layouts/15/appinv.aspx に移動します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/956246/c02d0749-84bc-5f93-72f1-939faa2f87ae.png)
先ほど生成した「クライアントID」を入力し
アプリの詳細が表示されたら、以下のようなアクセス許可XMLを入力します。必要な権限を指定します。以下はあくまでもサンプルです。
```xml
<AppPermissionRequests AllowAppOnlyPolicy="true">
    <!-- ドキュメント ライブラリへのフルアクセスする場合 -->
    <AppPermissionRequest Scope="https://(your-sharepoint-site)" Right="FullControl" />
    
    <!-- ドキュメントのアップロード -->
    <AppPermissionRequest Scope="https://(your-sharepoint-site)" Right="Add" />
    <!-- ドキュメントのダウンロード -->
    <AppPermissionRequest Scope="https://(your-sharepoint-site)" Right="Read" />
    <!-- ドキュメントの削除 -->
    <AppPermissionRequest Scope="https://(your-sharepoint-site)" Right="Delete" />
</AppPermissionRequests>
```
作成後、管理者は信頼するかを判断して承認します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/956246/e182598a-b48e-03ef-f081-55b2f1fda935.png)

https://(your-sharepoint-site)/_layouts/15/AppPrincipals.aspx で権限の確認ができます
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/956246/f70754c0-1218-d02f-5ca4-f314a5594855.png)