トレタ開発者ブログ

飲食店向け予約/顧客台帳サービス「トレタ」、モバイルオーダー「トレタO/X」などを運営するトレタの開発メンバーによるブログです。

Auth0のDelegated Administration Extension(DAE)を使ってユーザー管理を委譲してみよう

どうもみなさんおはこんばんにちは。最近はYoutuberのゲーム実況で見たゲームを買いがちなサーバサイドエンジニアの佐藤です。 この記事はトレタ Advent Calendar 2020の初日です!

突然ですが、Auth0、使ってますか?手軽に認証機能を追加できる便利なIdentity as a Service (IDaaS) ですが、弊社の様にBtoBでサービスを提供していると単に認証が行えるだけではなくユーザー管理を顧客側でやらせて欲しい、と言った要望を受けたり、我々がやるにしても数が多かったりしてお客様自身でやって欲しい!と思ってしまうこともあります。

そんな時にAuth0のユーザー管理機能を委譲、つまり代わってもらえる手段の一つにDelegated Administration Extensionがあります。 いろいろ検証した結果ちょっと今回のケースにはマッチせず本番利用はしていないのですが、何ができてどんなことを試したのかちょっとだけ公開したいと思います。

Delegated Administration Extension(DAE)とは

Auth0のユーザー管理・ログ閲覧ができるDashBoardを提供する、Auth0の拡張機能のことです。 私たち自身が何か実装することもなく、Extensionを有効にするだけで(最低限の)ユーザー管理機能を用意することができます。

加えてこのExtensionには Hooks と呼ばれるカスタマイズが可能で、ここにスクリプトを書くことでさらに細かくこのExtensionの挙動を弄る事が可能になります。 (拡張機能の拡張機能、って感じでちょっとややこしいですね)

Delegated Administration Extension(DAE)の使い方

初期設定

まずは公式のドキュメント通りにDAEをセットアップしていきます。 DAEのセットアップが完了したら、各種制御のためのHookの設定を行います。

Delegated Administration: Filter Hook

Delegated Administration: Filter Hook

Filter、という名前のとおりログイン後に表示されるユーザー一覧に出るユーザーを制御する事ができます。絞り込みはクエリによって行い、 Lucene query が使われます。

function(ctx, callback) {
  // app_metadataがあれば保存してあるorganizationを取得する
  var organization = ctx.request.user.app_metadata && ctx.request.user.app_metadata.organization;
  if (!organization || !organization.length) {
    return callback();
  }

  // 取得したorganizationと同一のorganizationを持つユーザーに絞り込むクエリを追加
  return callback(null, 'app_metadata.organization:"' + organization + '"');
}

auth0のapp_metadataには比較的好き勝手にいろいろ詰め込めるので、ユーザーを管理したいグループごとに分けられるようフラグを入れておくことにしましょう。今回は組織(Organization)というkeyを用意して、そこに組織名(ユニークなテキストならなんでもいい)を含めることにしました。

app_metadataにOrganizationが含まれていれば、同じOrganizationのみユーザー一覧に出るよう絞り込みます。含まれない場合には何もクエリを書かずにcallbackしていますが、これは弊社側の管理アカウントにOrganizationを含まないようにすることで、弊社の管理アカウントからは全ユーザーを横断で見れるようになってます。

後述するHookで弊社のアカウント以外は絶対Organizationを含んで保存されるように運用するので、一般ユーザーは常に自身のOrganizationに絞り込み表示されます。

今回は入れませんでしたが、 ctx.request.user.identities[0].connection でconnection、つまりどのDBのユーザーかを取得できます。 本番運用するならAuth0でメインで扱ってるconnectionとは別に、DAE用のconnectionを作ってその中でのみ運用する方が安全かと思います。

Delegated Administration: Write Hook

Delegated Administration: Write Hook

ユーザー情報が作成・更新される際に呼び出されるHookです。ここでapp_metadataを編集したり、内容に応じて処理を変えたりする事が可能です。

function(ctx, callback) {
  // 作成・更新者のorganization
  var userOrganization = ctx.request.user.app_metadata && ctx.request.user.app_metadata.organization;
  // 作成・更新するユーザーの情報
  var newProfile = ctx.payload;
  delete newProfile.memberships;
  
  // 作成・更新時にorganizationが指定されていなければ、作成・更新者のorganizationを引き継ぐ
  var organization = newProfile.app_metadata && newProfile.app_metadata.organization;
  if (!organization || !organization.length) {
    if (!userOrganization || !userOrganization.length) {
      return callback(new Error('The user must be created within a organization.'));
    } else {
      newProfile.app_metadata = {
        organization: userOrganization,
        ...newProfile.app_metadata
      }
    }
  }
  
  return callback(null, newProfile);
}

ここではユーザーの保存時にOrganizationを付加しています。Organizationの人がユーザーを追加・編集するときはそのOrganization内に限るはずだし限定したいので、暗黙的に操作したユーザーと同じOrganizationを設定しています。それ以外の場合(弊社側の管理者を想定)はOrganizationの指定が可能になっています。

Delegated Administration: Settings Query Hook

Delegated Administration: Settings Query Hook

DAEハックのキモ

ダイアログに追加のmetadataを表示させたり、作成時に指定できる選択肢を制御したり、果ては辞書翻訳に至るまで全てここ。ともかく userFields が一番肝要で、ここでmetadataの中身をダイアログに反映させています。やる気があればmailやパスワードにまで介入可能です。やれる事がやったら広いので長くなりがちですね。 function分けたりそのそも別ファイルでやりたい・・・

function(ctx, callback) {
  // 現在のユーザーのorganizationを取得
  let organization = ctx.request.user.app_metadata && ctx.request.user.app_metadata.organization;
  // 作成者にorgが設定されてなければ設定項目を表示するためのflg
  let canSetOrganization = (!organization || !organization.length);
  
  // organizationに関わるUI
  let userFieldOrganization = {
    "label": "Organization",
    "property": "app_metadata.organization",
    "search": {
      "display": true,
      "listOrder": 3,
      "listSize": 100,
    },
    "edit": {
      "type": "text",
    },
    "create": {
      "required": true,
      "type": "text",
    }
  };
  
  // Applicationに関わるUI
  let userFieldApplication = {
    "label": "Application",
    "property": "app_metadata.applications",
    "search": {
      "display": (function display(user, value, languageDictionary) { return value[0].map(x => x.label).join(",");}).toString(),
      "listOrder": 3,
      "listSize": 200,
    },
    "edit": {
      "type": "text",
      "component": "InputMultiCombo",
      "options": [{ "value": "daicho", "label": "台帳"}, { "value": "takeout", "label": "テイクアウト"}],
    },
    "create": {
      "type": "text",
      "component": "InputMultiCombo",
      "options": [{ "value": "daicho", "label": "台帳"}, { "value": "takeout", "label": "テイクアウト"}],
    }
  };
  
  let userFieldLocation = {
    "label": "Location",
    "property": "app_metadata.location",
    "search": {
      "display": true,
      "listOrder": 3,
      "listSize": 150,
    },
    "edit": {
      "type": "text",
    },
    "create": {
      "type": "text",
    }
  };
  
  let userFields = [userFieldApplication, userFieldLocation];
  if (canSetOrganization) {
    userFields.unshift(userFieldOrganization);
  }
  
  return callback(null, {
    // 作成できるconnectionをDAEのものに限定
    connections: [ 'Delegated-administration-extension' ],
    // 事前に用意した追加のフィールドを設定
    userFields: userFields,
    // ユーザーによるユーザーの作成を許可(権限があれば)
    canCreateUser: true,
    // 翻訳(本当はjsonのurlを指定する方が良いが、最低限試すならこの様に直指定できる)
    languageDictionary: {
      "userUsersTabTitle": "ユーザー",
      "userLogsTabTitle": "ログ",
      "userActionsButton": "操作",
      
      "cancelButtonText": "キャンセル",
      "updateButtonText": "更新",
      "createButtonText": "作成",
      "closeButtonText": "閉じる",
      
      "requiredFieldLabel": "(必須)",
      "requiredErrorText": "必須です",
    },
  });
}

userFieldほにゃらら で、追加のフィールドを定義しています。これはユーザーの作成・編集画面や、一覧画面に出る内容も制御が可能です。 search edit create あたりですね。 userFieldApplication がちょっと複雑になってますが、使用可能なアプリケーションを複数設定するために設定画面はコンボボックスに、一覧では , 区切りで並べて表示、というようなことをやってます。

f:id:k_sato_toreta:20201201101132p:plainf:id:k_sato_toreta:20201201101237p:plain
Hookで増えた要素

languageDictionary に値を流し込めば翻訳も可能です。直接指定の他に外部のjsonを指定できるので、どこかにアップロードしておけるならそっちの方がいいですね。翻訳可能な単語のリストもドキュメントにあります。どうやら自分で追加したフィールド等は対象外っぽかったので、そちらは初めから日本語で入れておきましょう。

その他

今回は使用していませんが、Hookから外部にリクエストすることも可能です。例えば、そのとき許可できるApplicationのリストを外部から取得した結果をフィードバックしてuserを作るようなことも可能でしょう。ユーザーの作成を連携する外部のシステムに通知したい、ということにも使えると思います。

まとめ

3行で言うと

  • Delegated Administration Extension(DAE)でユーザー管理画面を提供できる
  • そのままだと全部見えるため、Hooks で色々制限をかける
  • RoleやPermissionは弄れないがmetadataを付加・編集することができる

課題点

  • Roleの設定は不可。あくまでもユーザーだけ
    • DAEを使うにもRoleが必要なので、DAE完結では管理者を増やせない
  • Bulkエディットができない
    • 一括して権限付与、とか、ユーザーの大量作成が難しい。
  • ユーザー単位以外でデータ持てない
    • チームや部署などの単位を扱うのが難しい

これをそのままお客さんに提供しようと思うと足りない事が多くて辛いのですが、手軽さはすごいので社内ツール等で使用するにはいいんじゃないでしょうか? 「この管理画面はエンジニアさんにアカウントもらってください」なんて社内ツールあるあるじゃないですか?

個人的には結構面白さを感じたので、今後管理画面とか作る機会があったらまた再検討してみようかと思います。

最後に

それはで2日目以降のアドベントカレンダーも、お楽しみに! (本記事執筆時点で2日目空き枠だけど)


今年のトレタのアドベントカレンダーはこちら!

qiita.com

© Toreta, Inc.

Powered by Hatena Blog