Azure Functions (Python) でマネージド ID を使ってトークンを取得する

Azure Function のマネージド ID を有効化して、ローカルのエンドポイント (MSI_ENDPOINT) からトークンを取得してみます。

func newHttpTrigger を選択したときに生成されるコードを編集して、以下のコードを作成しました。

# __init__.py
import os
import logging

import azure.functions as func
from requests import get

def main(req: func.HttpRequest) -> func.HttpResponse:
    logging.info('Python HTTP trigger function processed a request.')

    # The two lines below are available only when managed ID enabled 
    msi_endpoint = os.getenv('MSI_ENDPOINT')
    msi_secret = os.getenv('MSI_SECRET')

    headers = {
        'secret': msi_secret
    }
    params = {
        'resource': 'https://vault.azure.net',
        'api-version': '2017-09-01'
    }
    response = get(url=msi_endpoint, headers=headers, params=params)

    logging.info(response.text)
    return func.HttpResponse('function was called')

resource の部分には、 Azure AD 認証をサポートするサービスを指定する必要があります。 今回は Key Vault を指定しました。 docs.microsoft.com

マネージド ID をローカルでテストする方法は今のところなさそうなので、 Function App に発行 (publish) して動作確認します。

Azure のサブスクリプション ID から Azure AD のテナント ID を取得する (Python)

Azure のサブスクリプション ID から Azure AD のテナント ID を取得する Python スクリプトです。

import requests

def get_tenantid_from_subscriptionid(subscriptionid):
  uri = 'https://management.azure.com/subscriptions/' + \
    subscriptionid + '?api-version=2015-01-01'
  headers = {
    'Authentication': 'Bearer xxx' 
  }
  resp = requests.get(uri, headers=headers)
  www_authenticate_header = resp.headers['WWW-Authenticate']
  tenantid = www_authenticate_header.split(',')[0].split('"')[1].split('/')[-1]
  return tenantid

参考

WWW-Authenticate の応答ヘッダーの値からテナント ID を取得します。 docs.microsoft.com

Azure のサブスクリプション ID から Azure AD のテナント ID を取得する

Azure のサブスクリプション ID から Azure AD のテナント ID を取得する PowerShell 関数です。

function Get-AzureADTenantIDFromSubscriptionID {
  param([parameter(Mandatory=$true)][String]$subscriptionId)

  $uri = "https://management.azure.com/subscriptions/${subscriptionId}?api-version=2015-01-01"
  $headers = @{
      'Authorization' = 'Bearer xxx'
  }

  try {
    Invoke-RestMethod `
      -Uri ${uri} `
      -headers ${headers}
  } catch {
    $wwwAuthenticateHeader = $_.Exception.Response.Headers["WWW-Authenticate"]
    $tenantId = ${wwwAuthenticateHeader}.split(',')[0].split('"')[1].split('/')[-1]
  }

  ${tenantId}
}

参考

WWW-Authenticate の応答ヘッダーの値からテナント ID を取得します。 docs.microsoft.com

PowerShell で ISO 8601 拡張形式の現在時刻を取得する

Get-Date の Format オプションに "o" または "O" を指定する。

Get-Date -Format "o"
# 2018-01-28T17:34:35.7418503+09:00

小数点以下が不要なら、 -replace 演算子で "." (ピリオド)から数字が連続する部分を削除する。

(Get-Date -Format "o") -replace "\.[0-9]+"
# 2018-01-28T17:34:41+09:00

SharePoint Online のサーバーリソースクォータに意味はない

Disclaimer: 本エントリは2018年1月5日現在の Office 365 の仕様に基づいています。

SharePoint Online のサイトコレクション単位で設定するサーバーリソースクォータ (Server Resource Quota) は、サンドボックスソリューションのために用意されている仕組みである。サンドボックスソリューションがサポートされなくなった今では、サーバーリソースクォータを設定することに意味はない。 SharePoint 管理センターのサイトコレクション作成の画面で既定で 300 という値が指定されているが、値を 0 にして(=クォータを設定せずに)サイトコレクションを作成しても問題ない。

f:id:umanohoneo:20180105015842p:plain Fig: サーバーリソースクォータ

不要なパラメータは一刻も早く無効化してほしいものだ。

Office 365 E3 ライセンスのうち Exchange Online (Plan 2) だけをユーザーに割り当てる

Disclaimer: 本エントリの内容は2018年1月3日時点の Office 365 の仕様に基づいています。

honeotech.hatenablog.com

上記のエントリでユーザーに Office 365 のライセンスを割り当てる方法について言及したが、組織によっては「 E1 を購入したが Exchange Online しか使わせたくない」といった場合もあると思うので、そのような場合のライセンス割り当ての方法を検証してみた。

検証したこと

Office 365 Enterprise E3 ライセンスのうち Exchange Online (Plan 2) だけをユーザーに割り当てる。

はじめに

Azure AD PowerShell v1 では Set-MsolUserLicense コマンドレットによってユーザーにライセンスを割り当てることができる。 E1 や E3 といった SKU の単位でユーザーにライセンスを割り当てることが可能であるが、それらに含まれるサービスごとのライセンスを個別に割り当てることも可能である。本エントリでは、 E3 に含まれる Exchange Online (Plan 2) だけをユーザーに割り当てる方法を示す。

SKU に含まれる特定のライセンスのみを割り当てるためには、割り当て対象のライセンスではなく、除外するライセンスを明示的に列挙する必要がある。例えば、今回のケースでは E3 に含まれる Exchange Online (Plan 2) 以外のすべてのライセンスを列挙する必要がある。この制約は、 New-MsolLicenseOption コマンドレットの仕様によるものである。

E3 に含まれるライセンスは将来的に変わる可能性があるため、はじめに E3 に含まれるライセンスの一覧を Office 365 から取得し、そこから Exchange Online (Plan 2) を除いて、「除外するライセンス」の一覧を作成することとする。

ライセンス割り当ての大まかな流れは次のとおり。

  1. E3 に含まれるライセンスの一覧を取得する
  2. Exchange Online (Plan 2) を除外したライセンスのセットを定義する
  3. ライセンスを割り当てる
  4. ライセンスの割り当て状況を確認する

E3 に含まれるライセンスの一覧を取得する

Get-MsolAccountSku コマンドレットで E3 (ENTERPRISEPACK)*1 のオブジェクトを取得し、そこからライセンスの情報を抽出する。

# Connect-MsolService が完了した状態で

# E3 ライセンスの AccountSku を取得する
$e3AccountSku = Get-MsolAccountSku | where {$_.AccountSkuId -match "ENTERPRISEPACK"}
$e3AccountSku
# 
# AccountSkuId              ActiveUnits WarningUnits ConsumedUnits
# ------------              ----------- ------------ -------------
# xxxxxxxxxx:ENTERPRISEPACK 25          0            8
# 
#
# E3 ライセンスに含まれる各サービスの ServiceName を取得する
$e3ServiceName = ($e3AccountSku | Select-Object -ExpandProperty ServiceStatus | Select-Object -ExpandProperty ServicePlan).ServiceName
$e3ServiceName
# BPOS_S_TODO_2
# FORMS_PLAN_E3
# STREAM_O365_E3
# Deskless
# FLOW_O365_P2
# POWERAPPS_O365_P2
# TEAMS1
# PROJECTWORKMANAGEMENT
# SWAY
# INTUNE_O365
# YAMMER_ENTERPRISE
# RMS_S_ENTERPRISE
# OFFICESUBSCRIPTION
# MCOSTANDARD
# SHAREPOINTWAC
# SHAREPOINTENTERPRISE
# EXCHANGE_S_ENTERPRISE

ここで BPOS_S_TODO_2 (To-Do (Plan 2)) から EXCHANGE_S_ENTERPRISE (Exchange Online (Plan 2)) までの17行が、 E3 に含まれるサービス(ライセンス)を表す文字列である。

Exchange Online (Plan 2) を除外したライセンスのセットを定義する

前の手順で取得したライセンスの一覧から Exchange Online (Plan 2) を除外し、ライセンスの一覧を再生成する。

# EXCHANGE_S_ENTERPRISE を除外した ServiceName の一覧を作成する
$serviceName = $e3ServiceName | sls -Pattern "EXCHANGE" -NotMatch
$serviceName
# 
# BPOS_S_TODO_2
# FORMS_PLAN_E3
# STREAM_O365_E3
# Deskless
# FLOW_O365_P2
# POWERAPPS_O365_P2
# TEAMS1
# PROJECTWORKMANAGEMENT
# SWAY
# INTUNE_O365
# YAMMER_ENTERPRISE
# RMS_S_ENTERPRISE
# OFFICESUBSCRIPTION
# MCOSTANDARD
# SHAREPOINTWAC
# SHAREPOINTENTERPRISE
# 
# 

次に、 New-MsolLicenseOption コマンドレットで LicenseOption オブジェクトを生成する。その際、 -DisalbedPlans オプションに前述のライセンス一覧を指定する。これにより、 Exchange Online (Plan 2) 以外のライセンスが無効化された E3 の LicenseOption オブジェクトが生成される。

# LicenseOption オブジェクトを作成する
$licenseOption =  New-MsolLicenseOptions -AccountSkuId 'xxxxxxxxxx:ENTERPRISEPACK' -DisabledPlans $serviceName

ライセンスを割り当てる

Set-MsolUserLicense コマンドレットでユーザーにライセンスを割り当てる。この時、 -LicenseOption オプションで、前の手順で作成した LicenseOption オブジェクトを指定する。なお、ユーザーに E3 ライセンスを既に割り当てている場合は、 -AccountSkuId オプションは省略可能である。

# ライセンスを割り当てる
Set-MsolUserLicense -UserPrincipalName <upn> -LicenseOptions $licenseOption -AddLicenses "xxxxxxxxxx:ENTERPRISEPACK"
# 何も出力されなければ成功

ライセンスの割り当て状況を確認する

最後に、 Get-MsolUser コマンドレットでライセンスの割り当て状況を確認する。

# ライセンスの割り当て状況を確認する
Get-MsolUser -UserPrincipalName <upn> | Select-Object -ExpandProperty Licenses | Select-Object -ExpandProperty ServiceStatus
# 
# ServicePlan           ProvisioningStatus
# -----------           ------------------
# BPOS_S_TODO_2         Disabled
# FORMS_PLAN_E3         Disabled
# STREAM_O365_E3        Disabled
# Deskless              Disabled
# FLOW_O365_P2          Disabled
# POWERAPPS_O365_P2     Disabled
# TEAMS1                Disabled
# PROJECTWORKMANAGEMENT Disabled
# SWAY                  Disabled
# INTUNE_O365           PendingActivation
# YAMMER_ENTERPRISE     Disabled
# RMS_S_ENTERPRISE      Disabled
# OFFICESUBSCRIPTION    Disabled
# MCOSTANDARD           Disabled
# SHAREPOINTWAC         Disabled
# SHAREPOINTENTERPRISE  Disabled
# EXCHANGE_S_ENTERPRISE Success  # Exchange Online (Plan 2) のみが割り当てられている
# 

Exchange Online (Plan 2) のみが割り当てられていることが確認できた。

*1:E1 の場合は STANDARDPACK

Python で Office 365 からメールを送信する

Disclaimer: 本エントリの内容は2018年1月3日時点の Office 365 の仕様に基づいています。

Python で Office 365 からメールを送信する方法を検証してみた。

検証条件

メールの送信者となる Office 365 のユーザーには多要素認証を適用していない。

使用するライブラリ

smtplib パッケージと email パッケージを使用する。 smtplilb パッケージは SMTP サーバ(Office 365)との通信に用いる。一方、 email パッケージはメッセージの組み立てに用いる。

なお、 Office 365 からメールを送るための専用のパッケージもあるようだが、 smtplib のほうが汎用性が高そうなので、勉強がてら今回は smtplib を使うことにした。

コード

必要最小限ではあるが、以下のコードで Office 365 からメール送信できる。smtp.ehlo() 以降の行には、 Python インタプリタで一行ずつ実行した際の出力(SMTP サーバからの応答)をコメントとして併記している。

import smtplib
from email.message import EmailMessage

# 接続先 SMTP サーバの定義
smtp = smtplib.SMTP('smtp.office365.com', 587)  # FQDN とポート番号

# 認証情報の定義
user = 'USER@DOMAIN'  # SMTP サーバに接続するためのユーザ
password = "PASSWORD"

# メッセージの組み立て
message = EmailMessage()
message['From'] = 'SENDER@DOMAIN'  # 送信者のメールアドレス
message['To'] = 'RECIPIENT@DOMAIN'  # 受信者のメールアドレス
message['Subject'] = 'SUBJECT'  # 件名
message.set_content('CONTENT')  # 本文

# SMTP サーバへの接続とメールの送信
smtp.ehlo()
    # (250, ...
smtp.starttls()  # TLS の開始(以降の通信は暗号化される)
    # (220, b'2.0.0 SMTP server ready') 
smtp.ehlo()
    # (250, ...
smtp.login(user, password)  # SMTP サーバにログイン
    # (235, ...
smtp.send_message(message)  # メッセージの送信
    # {}
smtp.quit()  # SMTP セッションの終了と TCP コネクションの切断
    # (221, b'2.0.0 Service closing transmission channel')

参考