Azure AD の動的グループに期限付きでユーザーを入れたい

Azure AD の動的グループに期限付きでユーザーを入れたい。

## 動機

Azure AD には動的グループという便利な仕組みがあります。
グループに対して事前に「メンバーシップ ルール」を設定しておけば、そのルールに合致しているユーザーやデバイスを自動でグループに含めてくれます。

この動的グループを利用して、あるユーザーを決められた日付まで、期限的にグループに入れる方法を考えてみます。

つまり、ユーザー属性のどこかに 2021-07-21 というように期限日を入力します。
今日（2021年7月6日）よりは未来の日付なのでユーザーはグループに含まれることになりますが、2021年7月22日になれば自動的にグループから抜けてほしいということです。
これで管理者はユーザーをグループから外すという作業をリマインダーに設定しなくて済むことになります。

想定されるユースケースとしては、期限的にライセンスを付与したり、アプリの利用を許可するといった場合がありそうです。

## 方針

期限日を入れる拡張属性として、適当な `extensionAttributeX` を使用します。
AD と Azure AD Connect の構築が面倒だったため、本稿では Azure AD から直接入力が可能な `companyName` で拡張属性を代用します。

拡張属性に今日以降の日付が入っていればグループに含む、そうでない（あるいは未入力）の場合はグループに含まないようにしてあげればよさそうです。
つまり、拡張属性に入力された日付と、今日の日付を表す変数 `today = 20210706` の大小関係を比較すれば完成でしょうか？

```js
// ※動きません
if (user.companyName >= today) {
  // 動的グループに含む
}
```

## 課題点

しかし、上記の方針で実現することはできませんでした。

- メンバーシップ ルールの値は定数しか入力できないため、変化する値（今日の日付 `today`）を表現できない
- メンバーシップ ルールの演算子に `-gt` (greater than) や `-lt` (less than) が存在しないため、YYYYMMDD 形式での単純な比較ができない

## 方針再考

どうすれば課題点を解消できるか考えてみます。

まず前者の変化する値ですが、今日の日付である `today` は1日1回しか変化しないため、日次バッチ処理で毎日0時0分に書き換えてあげればよさそうです。
Microsoft Graph API を使えば、スクリプトからルールを編集することができます。

後者の比較演算子の課題ですが、メンバーシップ ルールには `-match` という正規表現に利用される演算子があります。
「拡張属性に入力された日付が `today` 以上である」が真であるということを言い換えれば、「今日以降すべての日付を表す正規表現とマッチする」ということになるはずです。

いきなり実装に入ってもよいのですが、正規表現についてもう少し考えてみましょう。
「拡張属性に入力された日付 YYYY-MM-DD が `today` 以上である」を正規表現で扱いやすい形に分解してみます。

- 来年以降
- 今年だが来月以降
- 今月だが今日以降

擬似的にソースコードで書くと以下のようになります。

```js
// ※動きません
(YYYY > today.year) ||
(YYYY === today.year && MM > today.month) ||
(YYYY === today.year && MM === today.month && DD >= today.day)
```

等号や不等号は正規表現で扱いやすい形に変換します。
今日（2021年7月6日）を基準に当てはめてみると、

```js
// ※動きません
(YYYY -match "^20(2[2-9]|[3-9][0-9])$") -or
(YYYY -match "2021" -and MM -match "^(0[89]|1[0-2])$") -or
(YYYY -match "2021" -and MM -match "07" -and DD -match "^(0[6-9]|1[0-9]|2[0-9]|3[01])$")
```

実際は年・月・日で拡張属性を分けることなく、YYYY-MM-DD という形式で期限日を入力するため、条件を結合していきます。

```js
// ※動きません
(user.companyName -match "^20(2[2-9]|[3-9][0-9])-\d\d-\d\d$") -or
(user.companyName -match "^2021-(0[89]|1[0-2])-\d\d$") -or
(user.companyName -match "^2021-07-(0[6-9]|1[0-9]|2[0-9]|3[01])$")
```

## 実装

まずは今日の日付情報を取得します。

```js
const today = new Date();
const y = today.getFullYear().toString();
const m = ("0" + (today.getMonth() + 1)).slice(-2);
const d = ("0" + today.getDate()).slice(-2);
```

次に正規表現を表す連想配列を作成します。
今日の日付から動的に作成する方法も考えられますが、ロジックが複雑になるため事前に作成しておいた方がよいでしょう。

- 「来年以降」を表す正規表現
  - 2040年までしか作成していないので、2041年以降は正しく動きません。
  - バックスラッシュはエスケープ処理が必要です。

```js
const YEARS = {
  "2021": "^20(2[2-9]|[3-9][0-9])-\\d\\d-\\d\\d$",
  "2022": "^20(2[3-9]|[3-9][0-9])-\\d\\d-\\d\\d$",
  "2023": "^20(2[4-9]|[3-9][0-9])-\\d\\d-\\d\\d$",
  "2024": "^20(2[5-9]|[3-9][0-9])-\\d\\d-\\d\\d$",
  "2025": "^20(2[6-9]|[3-9][0-9])-\\d\\d-\\d\\d$",
  "2026": "^20(2[7-9]|[3-9][0-9])-\\d\\d-\\d\\d$",
  "2027": "^20(2[89]|[3-9][0-9])-\\d\\d-\\d\\d$",
  "2028": "^20(29|[3-9][0-9])-\\d\\d-\\d\\d$",
  "2029": "^20[3-9][0-9]-\\d\\d-\\d\\d$",
  "2030": "^20(3[1-9]|[4-9][0-9])-\\d\\d-\\d\\d$",
  "2031": "^20(3[2-9]|[4-9][0-9])-\\d\\d-\\d\\d$",
  "2032": "^20(3[3-9]|[4-9][0-9])-\\d\\d-\\d\\d$",
  "2033": "^20(3[4-9]|[4-9][0-9])-\\d\\d-\\d\\d$",
  "2034": "^20(3[5-9]|[4-9][0-9])-\\d\\d-\\d\\d$",
  "2035": "^20(3[6-9]|[4-9][0-9])-\\d\\d-\\d\\d$",
  "2036": "^20(3[7-9]|[4-9][0-9])-\\d\\d-\\d\\d$",
  "2037": "^20(3[89]|[4-9][0-9])-\\d\\d-\\d\\d$",
  "2038": "^20(39|[4-9][0-9])-\\d\\d-\\d\\d$",
  "2039": "^20[4-9][0-9]-\\d\\d-\\d\\d$",
  "2040": "^20(4[1-9]|[5-9][0-9])-\\d\\d-\\d\\d$",
};
```

- 「今年だが来月以降」を表す正規表現
  - 12月の次月は存在しないので、正規表現でマッチしなさそうな適当な値で埋めておきます。

```js
const MONTHS = {
  "01": `^${y}-(0[2-9]|1[0-2])-\\d\\d$`,
  "02": `^${y}-(0[3-9]|1[0-2])-\\d\\d$`,
  "03": `^${y}-(0[4-9]|1[0-2])-\\d\\d$`,
  "04": `^${y}-(0[5-9]|1[0-2])-\\d\\d$`,
  "05": `^${y}-(0[6-9]|1[0-2])-\\d\\d$`,
  "06": `^${y}-(0[7-9]|1[0-2])-\\d\\d$`,
  "07": `^${y}-(0[89]|1[0-2])-\\d\\d$`,
  "08": `^${y}-(09|1[0-2])-\\d\\d$`,
  "09": `^${y}-1[0-2]-\\d\\d$`,
  "10": `^${y}-1[12]-\\d\\d$`,
  "11": `^${y}-12-\\d\\d$`,
  "12": `^0001-01-01$`,
};
```

- 「今月だが今日以降」を表す正規表現

```js
const DAYS = {
  "01": `^${y}-${m}-(0[1-9]|1[0-9]|2[0-9]|3[01])$`,
  "02": `^${y}-${m}-(0[2-9]|1[0-9]|2[0-9]|3[01])$`,
  "03": `^${y}-${m}-(0[3-9]|1[0-9]|2[0-9]|3[01])$`,
  "04": `^${y}-${m}-(0[4-9]|1[0-9]|2[0-9]|3[01])$`,
  "05": `^${y}-${m}-(0[5-9]|1[0-9]|2[0-9]|3[01])$`,
  "06": `^${y}-${m}-(0[6-9]|1[0-9]|2[0-9]|3[01])$`,
  "07": `^${y}-${m}-(0[7-9]|1[0-9]|2[0-9]|3[01])$`,
  "08": `^${y}-${m}-(0[89]|1[0-9]|2[0-9]|3[01])$`,
  "09": `^${y}-${m}-(09|1[0-9]|2[0-9]|3[01])$`,
  "10": `^${y}-${m}-(1[0-9]|2[0-9]|3[01])$`,
  "11": `^${y}-${m}-(1[1-9]|2[0-9]|3[01])$`,
  "12": `^${y}-${m}-(1[2-9]|2[0-9]|3[01])$`,
  "13": `^${y}-${m}-(1[3-9]|2[0-9]|3[01])$`,
  "14": `^${y}-${m}-(1[4-9]|2[0-9]|3[01])$`,
  "15": `^${y}-${m}-(1[5-9]|2[0-9]|3[01])$`,
  "16": `^${y}-${m}-(1[6-9]|2[0-9]|3[01])$`,
  "17": `^${y}-${m}-(1[7-9]|2[0-9]|3[01])$`,
  "18": `^${y}-${m}-(1[89]|2[0-9]|3[01])$`,
  "19": `^${y}-${m}-(19|2[0-9]|3[01])$`,
  "20": `^${y}-${m}-(2[0-9]|3[01])$`,
  "21": `^${y}-${m}-(2[1-9]|3[01])$`,
  "22": `^${y}-${m}-(2[2-9]|3[01])$`,
  "23": `^${y}-${m}-(2[3-9]|3[01])$`,
  "24": `^${y}-${m}-(2[4-9]|3[01])$`,
  "25": `^${y}-${m}-(2[5-9]|3[01])$`,
  "26": `^${y}-${m}-(2[6-9]|3[01])$`,
  "27": `^${y}-${m}-(2[7-9]|3[01])$`,
  "28": `^${y}-${m}-(2[89]|3[01])$`,
  "29": `^${y}-${m}-(29|3[01])$`,
  "30": `^${y}-${m}-3[01]$`,
  "31": `^${y}-${m}-31$`,
};
```

以上の連想配列を利用して、適用するメンバーシップ ルールを作成します。

```js
const MEMBERSHIP_RULE = `
  (user.companyName -match "${YEARS[y]}") -or
  (user.companyName -match "${MONTHS[m]}") -or
  (user.companyName -match "${DAYS[d]}")
`;
```

Microsoft Graph API を使って、対象の動的グループのメンバーシップ ルールを編集します。
`Group.ReadWrite.All` のアクセス許可が必要です。
アクセストークン `access_token` とグループ ID `gid` は事前に取得しています。

```js
await axios.patch(
  `https://graph.microsoft.com/v1.0/groups/${gid}`,
  {
    membershipRule: `${MEMBERSHIP_RULE}`,
  },
  {
    headers: {
      Authorization: `Bearer ${access_token}`,
    },
  }
);
```

サンプルコードは [Gist](https://gist.github.com/sakaat/75bf85061e0ae0eca0f0665eebf029e1) に載せています。

## 運用

日付が変わった頃に毎日実行してあげればよさそうです。
正規表現を作成した2041年までは問題なく動く想定です。