Office365のサービス状況をChatworkに通知する


# はじめに
現場から「メールが受信できないんだけど」「OneDriveでエラーが」と言われてから、Office365の管理ポータルを覗くのものなぁと。サービス正常性に変更があれば、チャットツールであるChatworkに通知するようにした。本来は、MSFlowとかがやってくれるべきことのように思う。
Windows環境ではなかったのと、VisualStudioのサンプルコード・ライブラリが使えなかったこともあり、もう一回設定しろとなると？？？になりそうだったので、メモも兼ねてQiita投稿。

##想定読者
Office365の管理APIをつかって、いろいろと自動化して、楽をしたいOffice365管理者。

##参考URL
以下のリファレンスを見ながら作りました。
『管理APIスタートガイド』:
[Get started with Office 365 Management APIs](https://msdn.microsoft.com/ja-jp/office-365/get-started-with-office-365-management-apis)
『サービス通信APIリファレンス』:
[Office 365 Service Communications API reference (preview)](https://msdn.microsoft.com/office-365/office-365-service-communications-api-reference)

# おおざっぱな全体図
rubyで作ったプログラムをcronに登録して、定期的にo365のAPIを叩く、変更があればChatworkにポストという仕組みです。

管理APIリファレンスを見ると、o365の管理APIは、実行前に「契約主の許可」を得るフェーズを入れるのがメインのようです。アメリカなどでメジャーなMSP(Management Services Provider)のビジネスモデルで、MSPが管理APIを使用するという想定なんだと思います。

今回は、私自身が契約主なんで「契約主の許可」フェーズをすっ飛ばす方法で実装します。


# 大体の流れ
1. 事前準備
2. AzureADにアプリケーションを登録する
3. API呼び出しの認証情報を設定する
4. APIを使用するアプリケーションの開発

# 1. 事前準備
そもそも以下は必要です。
* Office365の契約があること
* Office365の管理者権限を持っていること

# 2. AzureADにアプリケーションを登録する
これから作るアプリケーションをAzureADに登録します。登録の手順は以下。
1. アプリケーションの登録
2. APIアクセスの許可
3. アプリケーション情報の取得

## 2-1. アプリケーションの登録
[AzurePortal](https://portal.azure.com/)にログインし、`アプリの登録 | 新しいアプリケーションの登録`メニューから、以下を入力。
　`名前`：任意（O365HealthCheckとか）
　`アプリケーションの種類`：Webアプリ/API（本当はWebアプリではないのだが、もう一方は未調査）
　`サインオンURL`：適当で良い
　
![00.appad.png](https://qiita-image-store.s3.amazonaws.com/0/43829/82124c7b-7dc7-93ea-fe02-659cf5a40f96.png "00.appad.png")





## 2-2. APIアクセスの許可
作成されたアプリケーションに、`プロパティの設定`画面から`必要なアクセス許可`をあたえる。
表示される画面で、`Office 365 Management API` を選択し、何をできるようにするかを選ぶ。
今回は`Read service health information for your organization`の権限があればOK。

![01.apiadd.png](https://qiita-image-store.s3.amazonaws.com/0/43829/79d1aa3a-17c0-775c-0c3a-82c9dcb7572d.png "01.apiadd.png")



## 2-3. アプリケーション情報の取得
API呼び出しに以下の情報が必要になる。どこかにとっておく。
それぞれ、API呼び出しにおいては括弧内の呼び名で使われる。

* アプリケーションID（`client_id`）
* OAUTH 2.0 トークン エンドポイントに含まれる契約者のID（`tenant_id`）

とくに`tenant_id`について、[『管理APIスタートガイド』](https://msdn.microsoft.com/ja-jp/office-365/get-started-with-office-365-management-apis)を見るとWebアクセスをして取得するような手順が紹介されているが、どうも管理画面から取得できる値と同じ。


`client_id`は、以下。
![アプリケーションID](https://qiita-image-store.s3.amazonaws.com/0/43829/baffaf3a-a45d-421b-64d6-10a1d9faba2c.png "appid.png")

`tenant_id`は、以下ののちに表示される各種URLのうち、
OAUTH2.0トークンエンドポイントのドメインの次に指定されたパス。
https://login.microsoftonline.com/xx(tenant_id)xx/oauth2/token
![b.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/43829/23913265-a6ff-935f-2bdb-53e2c6e9fc27.png)



![tenant_Id.png](https://qiita-image-store.s3.amazonaws.com/0/43829/807c7460-feae-f3f4-a186-a7cdf651588b.png "tenant_Id.png")


# 3. API呼び出しの認証情報を設定する
ここほんと厄介。とくにWindows環境でないと。
さらにOAuth2.0などの理解もきちんとしていないので、以下は「結果的にこれで動かせました。」というような内容。

1. 証明書を作成する。
2. AzurePortalに登録するデータを準備する。
3. Manifestの更新

## 3-1. 証明書の作成
秘密鍵とx.509証明書を用意する。
自己証明書で大丈夫だが、2048bit以上のキー長が必要。

## 3-2. AzurePortalに登録するデータの準備
Windows環境でやるならリファレンス通りで問題ないようだが、Mac環境だったので相当厄介だった。
AzurePortalに登録するのは、以下の3点。
1. X.509証明書の中身そのもの
2. X.509証明書のFingerprintを16進数のバイナリ配列とみなしてBase64符号化した文字列
3. GUID
厄介なのは、上記の2.「Fingerprintをバイナリ配列とみなして、Base64符号化した値」ですが、これは、非Windows環境では以下のようにして作れました。


```ruby
cert = OpenSSL::X509::Certificate.new(File.open(@cert_file))    
thumbprint = OpenSSL::Digest::SHA1.new(cert.to_der)
#16進数のバイナリ配列に変換
bin = Array(thumbprint.to_s).pack("H*")
#その後、Base64符号化
jwt_thumbprint = Base64.strict_encode64(bin)
                
```

## 3-3.Manifestの更新
AzurePortalのManifestに、上記の三点をしかるべき場所（14行目付近）に書き込んで、保存。

```json
"keyCredentials": [
  {
     "customKeyIdentifier" : "(上で取り出しFingerprintをBase64符号化した文字列)",
     "keyId": "（GUID)",
     "type": "AsymmetricX509Cert",
     "usage": "Verify",
     "value": "(証明書そのものをBase64符号化した値)"
  }
],
```


# 4. アプリケーションの開発
準備ができたので、アプリケーションを開発。
アプリケーションでやるべきことは、
・o365に投げるjwt_tokenをつくる。
・o365にjwt_tokenとclient_idを投げて、access_tokenを取得する。
・o365にaccess_tokenを投げて、サービス状態を取得するAPIを呼び出す。
・サービスに異常があれば、その内容をChatworkにポストする。
以下が、それように作ったクラス。


```ruby:CWebAppO365
    
# Office 365に接続するクラス
class CWebAppO365 
    attr_reader :http, 
        :client_id, 
        :cert_file,
        :tenant_id, 
        :access_token,
        :key_file,
        :jwt_token,
        :tmp_message

    def initialize(b_url, dbg = 0)
        
        # 設定ファイルから値を取り出す
        # ClientId = AzurePortalで「アプリケーションID」として表示されている値
        # TenantId = AzurePortalで、「OAUTH 2.0 トークン エンドポイント」として表示された値
        
        @client_id      = get_config("o365", "ClientId") 
        @tenant_id      = get_config("o365", "TenantId")
        @cert_file      = get_config("o365", "CertFile")
        @access_token   = ""
        @key_file       = get_config("o365", "KeyFile")
        @jwt_token      = makejwt()
        @tmp_message    = nil        
        
        uri = URI.parse(b_url)
        @http = Net::HTTP.new(uri.host, 443)
        @http.use_ssl = true
        
    end

    
    # アクセストークンの取得
    def prep()

        data = {"resource"              => "https://manage.office.com",
                "client_id"             => @client_id,
                "grant_type"            => "client_credentials",
                "client_assertion_type" => "urn:ietf:params:oauth:client-assertion-type:jwt-bearer",
                "client_assertion"      => @jwt_token}

		# アクセストークン取得のURIに、上記のdataをPOST。
		# dataに含まれる@jwt_tokenは、OAuthのjwtデータ makejwt()で作っている。
		# tenant_id は、AzurePortalから取得した値。
        target = "https://login.windows.net/" + @tenant_id + "/oauth2/token"

        uri = URI.parse(target)

        prehttp = Net::HTTP.new(uri.host, 443)
        req = Net::HTTP::Post.new(uri.path)
        req.set_form_data(data)

        prehttp.use_ssl = true

        res = prehttp.request(req)
        if (res.code != HTTP_OK) then
            raise "Error. posting code failure. (" + res.code + ")"
        end 
        
		#応答のjson形式のデータより、access_tokenを取り出す
        response_data = JSON.parse(res.body)
        @access_token   = response_data["access_token"]

    end

	# サービスの状態を取得
    def GetCurrentStatus()

        if (@access_token == "") then
            prep()
        end
        
		#アクセストークンをHTTPヘッダーに付与して、サービス状態を要求
        response = @http.get("/api/v1.0/" + @tenant_id + "/ServiceComms/CurrentStatus",
                            {"Authorization" => "Bearer " + @access_token})
        if (response.code != HTTP_OK) then
            raise "Error. Getting Status failure. (" + response.code + ")"
        end

		#応答のjson形式より、value部分を取り出す。
        data = JSON.parse(response.body)

        return data["value"]

    end

	# サービス異常の内容を取得する。
    def GetMessage(incidentId = nil)

        if (@tmp_message == nil) 
        
            if (@access_token == "") then
                prep()
            end
            
            response = @http.get("/api/v1.0/" + @tenant_id + "/ServiceComms/Messages",
                                {"Authorization" => "Bearer " + @access_token})
            if (response.code != HTTP_OK) then
                raise "Error. Getting Status failure. (" + response.code + ")"
            end

            data = JSON.parse(response.body)
            @tmp_message = data["value"]
        end

        ret = @tmp_message

        if (incidentId != nil) then
            @tmp_message.each { |msg|
                m_id        = msg["Id"]
                m_status    = msg["Status"]
                m_time      = msg["LastUpdatedTime"]

                if (incidentId == m_id) then
                    ret = msg
                    break     
                end
            }
        end

        return ret        
    end


    def getmsfingerprint(fprint)
    	# SHA1ハッシュ値を、16進数バイナリ配列として扱い、base64エンコード
        bin = Array(fprint.to_s).pack("H*")
        return Base64.strict_encode64(bin)
    end

	# jwt形式の認証トークンを生成する
    def makejwt()

		# azureportalに登録したX.509証明書
        cert = OpenSSL::X509::Certificate.new(File.open(@cert_file))
		# 秘密鍵        
        pkey = OpenSSL::PKey::RSA.new(File.open(@key_file))
       
		# 証明書のThumbprint
        thumbprint = OpenSSL::Digest::SHA1.new(cert.to_der)
		
		# AzurePortalに登録した形式に変換
        jwt_thumbprint = getmsfingerprint(thumbprint.to_s)
    
       # jwtのヘッダー    
        header = {
            :alg => "RS256",
            :x5t => jwt_thumbprint
        }

		# jwtのペイロード
        payload = {
            :aud => "https://login.windows.net/" + @tenant_id + "/oauth2/token",
            :iss => @client_id,
            :sub => @client_id,
            :jti => "13258bf2-5c80-4df6-8da2-793a2acae8be", #なんでもいいのか？
            :nbf => cert.not_before.to_i,
            :exp => cert.not_after.to_i   
        }
        
		# 秘密鍵でエンコード
        token = JWT.encode(payload, pkey, "RS256", header)
        
		# これがjwt形式のトークン
        @jwt_token = token

    end



end


```