からめもぶろぐ。

俺たちは雰囲気で OAuth をやっている

Power Apps の Office365Users.RelevantPeople 関数が結果を 10 件しか結果を返してくれない

Power Apps の Office 365 ユーザー コネクタにはユーザーに関連する人物をサジェストしてくれる RelevantPeople という関数があります。

docs.microsoft.com

しかしこの関数ですが、結果を 10 件しか返してくれません。

f:id:karamem0:20201001103301p:plain

その理由は、RelevantPeople 関数は Microsoft Graph の People API を呼び出しており、その既定値が 10 件しか値を返さないことによります。Microsoft Graph を直接呼び出すのであれば、OData クエリの $top を指定することによって結果の件数を変更することができるのですが、Office 365 ユーザー コネクタではその手段が提供されていません。よって 10 件以上の結果を取りたい場合はカスタム コネクタで Microsoft Graph を呼び出すことになります。

カスタム コネクタの作成についてはすでに素晴らしい記事が存在します。今回はこちらを参考にしながら手順を追っていきたいと思います。

idea.tostring.jp

手順

アプリケーションの登録

まずは Azure ポータルから Azure Active Directory にアプリケーションを登録します。

f:id:karamem0:20201001103844p:plain

[証明書とシークレット] から新しいクライアント シークレットを取得します。

f:id:karamem0:20201001103925p:plain

[API のアクセス許可] で「People.Read」を追加します。

f:id:karamem0:20201001104020p:plain

これで準備は完了です。

カスタム コネクタの作成

全般

以下の通りに設定します。

スキーマ HTTPS
ホスト graph.microsoft.com
ベース URL /
セキュリティ

認証タイプには OAuth を指定します。

ID プロバイダー Azure Active Directory
Client id (登録したアプリケーションのクライアント ID)
Client secret (登録したアプリケーションのクライアント シークレット)
Login URL https://login.windows.net
Tenant ID (登録したアプリケーションのテナント ID)
Resource URL https://graph.microsoft.com
スコープ https://graph.microsoft.com/.default
定義

要求の [サンプルからインポート] より以下のように入力します。

動詞 GET
URL https://graph.microsoft.com/v1.0/me/people?$top=100

応答は Graph Explorer を使って https://graph.microsoft.com/v1.0/me/people を叩いてみて(ログインする必要はありません)その結果の JSON を [サンプルからインポート] で登録すればよいでしょう。

ここまでできたらいったんカスタム コネクタを保存します。

リダイレクト URL の登録

Azure ポータルに戻り、カスタム コネクタの [セキュリティ] に表示されているリダイレクト URL をアプリケーションに登録します。

f:id:karamem0:20201001111249p:plain

これでカスタム コネクタの設定は完了です。念のため接続を作成してテストを実行し結果が返ってくることを確認するといいでしょう。

実行

カスタム コネクタを実行してみます。$top を指定することができるようになっており、10 件以上結果が返ってくるのがわかります。*1 この方法を使って、$filter を指定できるようにしてあげれば、結果として返ってくるユーザーからグループを除外したり、組織内のユーザーに限定したりすることが可能となります。

f:id:karamem0:20201001111737p:plain

*1:キャプチャは 100 を指定したのに 50 件しか取ってこれていませんがもともと 50 件しかないだけです。

Power Automate で CSV を保存したときにコンテンツが文字化けする

Power Automate で CSV を作成して OneDrive for Business などに保存するフローを作ったときに、CSV のコンテンツに日本語が含まれると Excel で開いたときに文字化けしてしまいます。

たとえば、以下のような JSON を CSV に変換して OneDrive for Business に保存するフローを作成してみます。

[
  { "Id": 1, "Title": "日本書紀" },
  { "Id": 2, "Title": "万葉集" },
  { "Id": 3, "Title": "古事記" }
]

フローは以下のような感じになります。

f:id:karamem0:20200930163824p:plain

これを実行して Excel で開くと文字化けしているのがわかります。

f:id:karamem0:20200930163839p:plain

原因

Power Automate では文字列はすべて UTF-8 で処理されます。よって OneDrive for Business に保存したときも文字コードは UTF-8 になります。しかし Excel は UTF-8 であることを認識できずに Shift-JIS でファイルを開いてしまいます。UTF-8 であることを認識させるには BOM 付きの UTF-8 として保存してあげる必要があります。

対応方法

ファイルを保存するときに BOM を追加してあげます。BOM は 0xEF 0xBB 0xBF の 3 バイト データなので、これを追加するようフローを修正します。バイト データの書き出しに decodeUriComponent('%EF%BB%BF') を使うのがポイントです。

f:id:karamem0:20200930163856p:plain

これでちゃんと文字化けしないで出力されるようになりました!

f:id:karamem0:20200930163908p:plain

SharePoint を使ったバッチ プログラムの認証/承認がなぜこんなにややこしいのか考えてみる

SharePoint を使ったバッチ プログラム開発、簡単なものでは手元で実行する簡単なデータ投入ツールだったり、複雑なものになると Azure Functions や Azure Automation を使ってやるものだったりすることがありますが、いずれにしてもこうした無人化されたプログラムでの認証/承認は避けられない問題です。SharePoint はオンプレミスからクラウドへと進化し続ける製品であり、その過程によってさまざまな試行錯誤があったため、結果として互換性の観点から複雑になってしまっているところがあります。大げさではありますが、歴史を振り返ることで、今は何がベストの方法なのかを考えてみたいと思います。

クラウド以前

SharePoint Online が世の中に広まるずっと以前、オンプレミスの SharePoint が全盛期だった時代、バッチ プログラムは SharePoint サーバーの中で動かすのが基本でした。SharePoint サーバー オブジェクト モデル (Microsoft.SharePoint.dll) を使って、ファーム アカウントで動作させることで、バッチ プログラムを非常に強い権限で動かすことができました。また、SharePoint 2010 からはクライアント オブジェクト モデル (Microsoft.SharePoint.Client.dll) が誕生し、外部のサーバーから SharePoint を操作することができるようになりましたが、ほとんどの場合は NTLM 認証で事足りたため、ファーム アカウント (またはそれに近い強い権限を持つアカウント) のユーザー名とパスワードでログインするということをしていました。

クラウド転換期

SharePoint 2013 から、それまでのファーム ソリューション開発やサンドボックス ソリューション開発に代わる新しい開発手法として、SharePoint アドイン (当時は SharePoint アプリと言っていました) が誕生しました。SharePoint アドインの特長として、アプリを SharePoint に登録することで、アプリに必要なアクセス許可を指定することができるという点がありました。これまでは「アプリ自体に何をさせることができるか」が指定できなかったため、悪意のある操作もできてしまったのですが、SharePoint アドイン モデルを採用することにより、アプリを制限された権限の範囲内で動作させることが可能になりました。SharePoint アドインの認証/承認には Azure ACS を使った OAuth の仕組みが取り入れられており、SharePoint Server でも SharePoint Online でも同じように動作させることができました。同じく SharePoint 2013 から導入された REST API と組み合わせて、これまで .NET でしか開発できなかったバッチ プログラムを他の言語でも開発できるようになりました。また、ユーザーがアプリに動作を委任するのではなく、アプリそのものがサービス アカウントとして動作する仕組み (Client Credentials Grant) もできました。

他方でクライアント オブジェクト モデルは進化をし続きます。SharePoint Online に対応するために SharePointOnlineCredentials (WS-Security) による認証により、ほぼ NTLM 認証と同じ感覚で開発をすることができました。

クラウド全盛期

Azure の発展や Office 365 によるサービスのクラウド化による「すべてがクラウドへ」という動きが加速するにつれ、SharePoint もクラウド ファーストという考えが入ってきます。SharePoint Server は SharePoint Online の過去のバージョンをパッケージ化したものであり、新しい機能は SharePoint Online にのみ取り入れられるようになります。Microsoft のクラウド サービス間の連携の機運が高まり、Azure Active Directory を基盤としてすべてのサービスに繋がる仕組みができます。これはのちに Microsoft Graph へと繋がっていきますが、SharePoint Online をはじめとした Microsoft のクラウド サービスの REST API は Azure Active Directory にアプリを登録して、OAuth による認証/承認のプロセスを経ることになります。SharePoint はここにきてまったく同じ仕組み (OAuth) を持つ異なる認証/承認方法を持つようになってしまいます。

SharePoint アドイン モデル Azure Active Directory
SharePoint Server 動作する 動作しない
SharePoint Online 動作する 動作する
他サービス連携 動作しない 動作する

SharePoint Server がある以上、互換性のために、SharePoint アドイン モデルによる OAuth をやめてしまうことはできません。Azure ACS は廃止されましたが、SharePoint アドイン モデルによる OAuth は生き残りました。SharePoint Online でバッチ プログラムを動かすときには、SharePoint アドインとしてアプリ登録することも、Azure Active Directory にアプリ登録することもでき、現時点ではどちらも有効な方法です。

クライアント オブジェクト モデルには .NET Core の波が押し寄せます。長らくクライアント オブジェクト モデルは .NET Standard に対応していなかったのですが、ついに .NET Standard に対応したライブラリが誕生しました。その際、もはや古い手法でありセキュリティにも問題がある SharePointOnlineCredentials を切り捨てています。今後、SharePointOnlineCredentials は使えなくなる流れにあると考えていいでしょう。

blog.karamem0.jp

まとめ

現時点で、SharePoint Online のバッチ プログラムを開発する上で、SharePoint アドイン モデルと Azure Active Directory のどちらの認証/承認を使っても問題はありません。ただし、SharePoint アドイン モデルはサポートされているとはいえ、すでに開発方法自体が古いものであり、今後何の前置きもなしに無効化されてしまう可能性を否定できません。ただし、SharePoint Online では Azure Active Directory のクライアント シークレットを使った Client Credentials Grant がサポートされないため、証明書を使う必要があります。クライアント シークレットを使うほうがお手軽なため、上記のリスクを承知したうえで、SharePoint アドイン モデルによる OAuth を使うという考え方はできると思います。

Office 365 管理 API を使って SharePoint Online の監査ログを取得する

いままでずっと SharePoint Online の監査ログの生データは取得できないと思っていたのですが、実は Office 365 管理 API というものを使えば取得できるのだそうです。Office 365 管理 API は SharePoint、Exchange、Teams、Power Platform などのさまざまな監査ログを取得できるのですが、特に利用状況の監視目的で SharePoint Online の監査ログを取りたいという要望は非常に多いため、この方法を使えば解決できるのはないかと思います。

なお今回は以下の記事を参考にしています。

qiita.com

サンプル コード

github.com

実行手順

Office 365 管理 API でログを取得するには以下の手順が必要になります。

  • アクセス トークンを取得する
  • サブスクリプションを作成する
  • コンテンツの URL を取得する
  • コンテンツを取得する

アクセス トークンを取得する

Office 365 管理 API を使用するためには、はじめに Azure Active Directory にアプリを登録する必要があります。[API のアクセス許可] では [Office 365 Management APIs] - [アプリケーションのアクセス許可] - [ActivityFeed.Read] を追加します。

f:id:karamem0:20200902135316p:plain

またアプリケーション シークレットも取得しておきます。

Office 365 管理 API は Azure Active Directory の v2.0 エンドポイントに対応していないので MSAL を使うことができません。ADAL を使ってもいいのですが、面倒なので、今回は直接 HttpClient で取りに行くようにします。といっても Client Credentials Grant なのでそれほど難しくはありません。

private static void AcquireToken()
{
    var httpRequestUrl = $"https://login.microsoftonline.com/{TenantId}/oauth2/token";
    var httpRequestMessage = new HttpRequestMessage(HttpMethod.Post, httpRequestUrl);
    var httpRequestContent = new FormUrlEncodedContent(new Dictionary<string, string>()
    {
        { "grant_type", "client_credentials" },
        { "resource", Resource },
        { "client_id", ClientId },
        { "client_secret", ClientSecret }
    });
    httpRequestMessage.Content = httpRequestContent;
    var httpResponseMessage = HttpClient.SendAsync(httpRequestMessage).GetAwaiter().GetResult();
    var httpResponseContent = httpResponseMessage.Content.ReadAsStringAsync().GetAwaiter().GetResult();
    var httpResponseJson = JsonConvert.DeserializeObject<JToken>(httpResponseContent);
    AccessToken = httpResponseJson.Value<string>("access_token");
}

サブスクリプションを作成する

Office 365 管理 API のコンテンツ タイプ (Audit.SharePoint) に対してサブスクリプションを作成します。Webhook も登録できるようなのですが、今回は省略します。PublisherIdentifier は任意の GUID を指定してください。

private static void CreateSubscription()
{
    var httpRequestUrl = $"https://manage.office.com/api/v1.0/{TenantId}/activity/feed/subscriptions/start?contentType=Audit.SharePoint&PublisherIdentifier={PublisherIdentifier}";
    var httpRequestMessage = new HttpRequestMessage(HttpMethod.Post, httpRequestUrl);
    httpRequestMessage.Headers.Authorization = new AuthenticationHeaderValue("Bearer", AccessToken);
    var httpResponseMessage = HttpClient.SendAsync(httpRequestMessage).GetAwaiter().GetResult();
    var httpResponseContent = httpResponseMessage.Content.ReadAsStringAsync().GetAwaiter().GetResult();
}

コンテンツの URL を取得する

ログを取得する時間を指定してコンテンツ データのダウンロード先の URL を取得します。時間は 24 時間以内で過去 7 日以内である必要があります。今回は 1 日前のデータを取得するように指定します。

private static void GetContentUri()
{
    var startTime = DateTime.Today.AddDays(-1).ToUniversalTime().ToString("yyyy-MM-dd'T'HH:mm:ss");
    var endTime = DateTime.Today.AddSeconds(-1).ToUniversalTime().ToString("yyyy-MM-dd'T'HH:mm:ss");
    var httpRequestUrl = $"https://manage.office.com/api/v1.0/{TenantId}/activity/feed/subscriptions/content" +
        $"?contentType=Audit.SharePoint" +
        $"&PublisherIdentifier={PublisherIdentifier}" +
        $"&startTime={startTime}" +
        $"&endTime={endTime}";
    var httpRequestMessage = new HttpRequestMessage(HttpMethod.Get, httpRequestUrl);
    httpRequestMessage.Headers.Authorization = new AuthenticationHeaderValue("Bearer", AccessToken);
    var httpResponseMessage = HttpClient.SendAsync(httpRequestMessage).GetAwaiter().GetResult();
    var httpResponseContent = httpResponseMessage.Content.ReadAsStringAsync().GetAwaiter().GetResult();
    var httpResponseJson = JsonConvert.DeserializeObject<JArray>(httpResponseContent);
    ContentUri = httpResponseJson[0].Value<string>("contentUri");
}

コンテンツを取得する

取得した URL から JSON 形式のコンテンツを取得します。

private static void GetContents()
{
    var httpRequestUrl = ContentUri;
    var httpRequestMessage = new HttpRequestMessage(HttpMethod.Get, httpRequestUrl);
    httpRequestMessage.Headers.Authorization = new AuthenticationHeaderValue("Bearer", AccessToken);
    var httpResponseMessage = HttpClient.SendAsync(httpRequestMessage).GetAwaiter().GetResult();
    var httpResponseContent = httpResponseMessage.Content.ReadAsStringAsync().GetAwaiter().GetResult();
    var httpResponseJson = JsonConvert.DeserializeObject<JArray>(httpResponseContent);
    Console.WriteLine(JsonConvert.SerializeObject(httpResponseJson, Formatting.Indented));
}

実行結果

以下のような JSON が出力されます。サンプルはファイルのダウンロードですが、それだけではなく、いろいろな種類のログが取得できます。

[
  {
    "CreationTime": "2020-09-01T14:00:03",
    "Id": "bea6430c-9db6-400c-ebbf-08d84e7f532c",
    "Operation": "FileDownloaded",
    "OrganizationId": "92dbed3f-d37a-4f19-a392-f6970505cc6a",
    "RecordType": 6,
    "UserKey": "i:0h.f|membership|10033fffac7f4b34@live.com",
    "UserType": 0,
    "Version": 1,
    "Workload": "OneDrive",
    "ClientIP": "52.185.144.178",
    "ObjectId": "https://karamem0jp-my.sharepoint.com/personal/takashi_shinohara_karamem0_jp/Documents/fitbit.json",
    "UserId": "takashi.shinohara@karamem0.jp",
    "CorrelationId": "1aba0151-abda-4b69-a102-a9319a2a9e15",
    "EventSource": "SharePoint",
    "ItemType": "File",
    "ListId": "ccf377fa-0605-41b5-925a-224e62839884",
    "ListItemUniqueId": "3afd549b-4c58-4026-a7e2-d4fcc1a2ebe0",
    "Site": "0815189e-2f76-44f2-89fe-f2dd7260e20d",
    "WebId": "15cdf073-9e78-4667-94ea-96abe6aa860f",
    "HighPriorityMediaProcessing": false,
    "SourceFileExtension": "json",
    "SiteUrl": "https://karamem0jp-my.sharepoint.com/personal/takashi_shinohara_karamem0_jp/",
    "SourceFileName": "fitbit.json",
    "SourceRelativeUrl": "Documents"
  },
...
]

何が取得できるかについては以下が参考になります。

docs.microsoft.com

まとめ

Office 365 管理 API に関する情報は以下にまとまっています。

docs.microsoft.com

オンプレの SharePoint で監査ログを取っていろいろやっていたことを、SharePoint Online に移行しても同じことをやりたい、というときにぜひ使っていただきたいと思います。