Visual Studio Codeで.NET Core開発をする(Mac)

最近C#を触ることが増えてきたのですが、さすがに2013年モデルのMBPでParallels+Visual Studioではいろいろと(特にビルド時間)つらくなってきたので、小規模な個人プロジェクトはVisual Studio Codeでやってみようと思い、環境を整えてみました。その記録を残しておきます。

まず、基本的には公式の以下ページの通りで比較的簡単に開発を始められます。

code.visualstudio.com

.NET Core SDKのインストール

若干つまづいたのは.NET Core SDKをインストールしたあと、dotnetコマンドが使えなかった点でした。

$ dotnet
zsh: command not found: dotnet

/usr/local/share/dotnet/dotnetにバイナリは存在していたので、ひとまずシンボリックリンクを作ることにしました。

# dotnetコマンドの存在チェック
$ ls -al /usr/local/share/dotnet/dotnet
-rwxr-xr-x  1 root  wheel  105872  3 19 07:05 /usr/local/share/dotnet/dotnet

# シンボリックリンク作成
$ ln -s /usr/local/share/dotnet/dotnet /usr/local/bin/

関連するStackOverflowが以下のリンク先にありますが、執筆時点ではCatalinaが影響している可能性がありそうですね。

stackoverflow.com

コンソールアプリの作成

dotnetコマンドの準備もできたところでコンソールプロジェクトをさっそく作って、Visual Studio Codeで開きます。

# Consoleプロジェクト作成
$ dotnet new console

# Visual Studio Codeで開く
$ code .

すると、下図のように「いろいろC#用のアセットをインストールしますか?」って聞かれるので「YES」を選択します。

f:id:kenev:20200509094329p:plain:h200

完了しましたらターミナルで実行してみましょう。ビルド状況を少し可視化しておきたかったので-v n オプションをつけています。

$ dotnet run -v n

# ...割愛

ビルドに成功しました。
    0 個の警告
    0 エラー

経過時間 00:00:01.63
Hello World!

いとも簡単に実行に成功してます。

ただ、いちいちターミナルから実行してたらDX(Developer Experience)が微妙なので、Visual Studio CodeのアクションでTasks: Run Taskを実行します。

f:id:kenev:20200509100028p:plain

そうすると、下図のようにwatch, build, publishが用意されています。この中身は.vscode/tasks.jsonでも確認することができます。

f:id:kenev:20200509100056p:plain

watchを実行することでプログラムがコンパイルされて実行されます。

f:id:kenev:20200509100209p:plain

そして、名前の通り変更を加えて保存すると自動で再コンパイルして実行もしてくれます。

下図は「Hello World!」を「Hello 世界!」に変えた実行結果。

f:id:kenev:20200509100231p:plain

この時点で手軽さにかなり感動しました!Macでもすぐに始められます(ただし、本格的なアプリを作るのはまだこれから)

デバッグ

それでは、デバッグはどんな感じなのか試してみます。ブレイクポイントはエディタの行番号の横をクリックすることで赤丸がついてくれます。

f:id:kenev:20200509225352p:plain

左のメニューから「Run」を選択し、.NET Core Launch(console)を実行します。

f:id:kenev:20200509225015p:plain:h200

しばらくすると普通にブレイクポイントで止まってくれます。Step In, Step Out, Step Overなど当たり前にできますし、変数の中身も問題なく変えられます。

Visual Studioを使っていると当たり前なのですが、まさかVisual Studio Codeでここまで普通にできるとは思っていなかったです。

f:id:kenev:20200509225723p:plain

おまけ

せっかくなので他の機能もチェックしてみました。

リファクタリング

定数を抽出してから、その定数の名前を変更するリファクタを試してみました。

文字列"Hello 世界!"を定数Valueに抽出するにはカーソルをあわせて豆電球をクリックするかoption+enterで選択肢が表示されるので、実行したい変更を選びます。

また、このときにValueという定数名が勝手につけられていて変えたいと思ったので、Valueにカーソルを合わせてF2をクリックし、定数名の変更を実行してみました。

f:id:kenev:20200509155656g:plain
定数抽出からの定数名変更

名前空間の追加とIntellisense

次に、不足している名前空間の追加もやってみました。例えばusing System;が無い状態で、Consoleと書いてみます。本来手動でusing System;と追記しなければいけないところ、上の例と同様でこのときに豆電球が表示されるので、クリックするかoption+enterを入力するとusing System;という選択肢が表示されます。これを選択すると、自動的にファイルの先頭にusing System;が追加されます。

さらに、Intellisenseもばっちりです。Console.と、ドットと入力した時点で、メソッドの候補がずらっと並びますし、入力するにつれて候補が絞られていきます。

f:id:kenev:20200509155745g:plain
名前空間追加&コード補完(Intellisense)

以上で、さくっとVisual Studio Codeで.NET Coreの開発を始めてみました。数年前の自分であればこんなにシンプルにC#での開発を始められるなんて(しかもMacで)想像もしていなかったです。

最近発表のあった、Codespacesが使えるようになったらさらに気軽にC#の開発ができるようになりそうなのでかなり楽しみですね。

github.com

これから本格的に個人プロジェクトもこれで作ってみようと思います。

まとめ

  • Visual Studio Codeで.NET Coreの開発環境を構築した
  • 特に複雑なインストール手順もなく、リファクタリングやIntellisenseなどの高機能が使える
  • .NET Coreの開発が一昔前よりかなりハードルが下がっていると感じた

読書をマインドマップでまとめる / 「マインドマップ読書術」を読んだ

数年前からマインドマップでいろいろな情報を残すようにしているのですが、ふとAmazonブラウジングしてたときに気になる本があったので買って読んでみました。「マインドマップ読書術」という本です。100ページ弱の小型の本でサクッと読めちゃいますが、読んだ記録を残しておこうと思います。

目次

  1. あなたは4倍速く読める!
  2. 問題点を逆手に取る
  3. 新たな目の使い方を学ぶ
  4. さまざまな読み方を使いこなす
  5. 集中力をコントロールする
  6. マインドマップ有機的学習法MMOST
  7. マインドマップ読書ノート術

読み方の思い込みを捨てる

まず、この本ではマインドマップの「書き方」ではなく、マインドマップを活用した効率的な読書の方法(読書術)を紹介していることに注意する必要があります。

戦略的にプレビューする

プレビューで「先に・見る」ことに関して、3つのポイントが挙げられています。

中でも上2つのポイントが刺さりました。

1つ目は、本を読む前にその本の分野に関しての自分の既存知識をマインドマップにしておくというものです。なんとなくボヤッと頭の中にある既存知識をあらかじめまとめておくと、知っていることが整理され、本を読みながら答え合わせができます。

2つ目は、著者と会話をするように(インタラクティブに)読むべきだというものです。具体的には本の余白に質問やコメントを残したりすることです。読書の際、著者との会話は忘れがちで、気づくとだらだらっと数ページ読んでいて全く内容が頭に入っていなかったりします。本自体に質問やコメントを残すことを意識しておくことで、集中力を高めます。Inputだけでは眠くなってしまうので、定期的にOutputすることを忘れずに読書することが大事だと感じました。

読み方を工夫する

本を読むとき、1回読んで満足してしまいがちですが、上にも述べたようにほとんど知識として定着しません。ここ数年で僕もようやく定着してきたのですが、複数回読むほうが圧倒的に定着します。そのテクニックとして本書ではMMOST(Mind Map Organic Study Technique)が紹介されています。マインドマップの父であるトニー・ブザン氏が考案したテクニックのようです。

準備の4ステップ

  1. ブラウジング」で、全体にざっと目を通す
  2. 読書の「時間と量」を決める
  3. 既存知識の「マインドマップ」を描く
  4. 「質問」をつくり「目標」を定義する

応用の4ステップ

  1. 「概略」を読む
  2. 「要点」を読む
  3. 「詳細」を読む
  4. 「仕上げ読み」する

「丁寧に1回読む」ことをやめて、「複数回読む」前提で本を読む必要があります。MMOSTに関しては(英語になりますが)以下の記事でも紹介されています。

Mind Map Organic Study Technique (MMOST)destech.wordpress.com

まとめ

  • マインドマップ読書術」を読んだ
  • 本は複数回読む方が良い
  • 「本を読むこと」自体を目的化しちゃいけない

余談ですが、下手なりにマインドマップをアプリではなく、Apple Pencilで書いたものが以下のとおり。

f:id:kenev:20200507203524j:plain

参考

トニーブザンのマインドマップ本(マインドマップの書き方)

Mind Map Mastery (English Edition)

Mind Map Mastery (English Edition)

記憶術に関するUdemyのコース

www.udemy.com

マインドマップツール「MindMeister」(Mind Mapを横断的に検索できて良い)

www.mindmeister.com

Trelloのタスクやり忘れを無くすためにSlackと連携した

f:id:kenev:20200408192644p:plain

Trelloに「今日やること」を入れるようにしてるのですが、どうにもこうにも夕方ごろには忘れて、やり残してしまうタスクがあったりします。どうにかこれを改善できないかと思い、GAS(Google App Scripts)とSlackを連携する簡単なスクリプトを作ることにしました。

スクリプトの概要

この記事で紹介するスクリプトで僕がやりたかったことはとてもシンプルで、

「Trelloの "Do Today" リストに含まれているカードをSlackに定期的に通知する」

というものです。これを実現するためには以下が必要となります。

  • 指定したTrello Boardの指定したListに含まれるカード一覧を取得
  • カード一覧を整形してSlackにIncoming Webhook経由で通知
  • GASのTriggerで定期的にスクリプトを実行

記事を読みたくない人へ

GASもSlackのWebhookもだいたいわかってるという人は:

上記を用意しておけば、以下のスクリプトをGASに配置して動かすことができます。

Trello List to Slack

これより下で、設定方法を詳しく見ていきます。

Trello→Slack通知の作り方

それでは、作り方を順番に見ていきます。

前提知識

この記事は親切丁寧にすべてのステップは説明しません。以下の点についてはある程度わかっているものとします。

  • GASを知っている(1個くらい作ったことある)
  • SlackでIncoming Webhookを使ったことがある

「全くわからない!」という方は入門記事やチュートリアルを試してみることをおすすめします!

Trello BoardのListに含まれるカード一覧を取得

まずはGASでTrello Boardの指定したListからカード一覧を取得します。

以下記事を参考にほとんどそのまま使わせていただきました。

qiita.com

上記の記事の手順に従って

  • TrelloのAPIキー(①)
  • Trelloのトークン(②)
  • Trelloのユーザー名(③)

を取得してください。下のスクリプトで使います。

次にGASの新規プロジェクトを用意します。

script.google.com

スクリプトには以下を記載します。

// --------------EDIT REQUIRED START---------------------
// Trello
var TRELLO_USERNAME = "<USERNAME>";// enter your trello username
var TRELLO_KEY   = "<TRELLO KEY>";// enter your trello key
var TRELLO_TOKEN = "<TRELLO TOKEN>";// enter your trello token
var TRELLO_BOARD_NAME = "<TRELLO BOARD NAME>"; // enter your trello board name
var TRELLO_LIST_NAME = "<TRELLO LIST NAME>"; // enter your trello list name
// --------------EDIT REQUIRED END---------------------

var TRELLO_CREDENTIALS = "key=" + TRELLO_KEY + "&token=" + TRELLO_TOKEN;

function main() {
  var cards = findTrelloCardsFromList(TRELLO_BOARD_NAME, TRELLO_LIST_NAME);
  if (cards.length === 0) {
    // don't notify if cards are empty
    return;
  }
  
  Logger.log(cards);
}

function findTrelloCardsFromList(boardName, listName) {
  var boardId = findTrelloBoardId(boardName);
  var listId = findTrelloListId(boardId, listName);
  
  var url = "https://trello.com/1/lists/" + listId + "/cards?&fields=name,shortUrl&" + TRELLO_CREDENTIALS;
  var res = UrlFetchApp.fetch(url, {'method':'get'});
  var cards = JSON.parse(res);
  
  return cards;
  
  ///////////// function implementations
  
  function findTrelloBoardId(name) {
    var url = "https://trello.com/1/members/" + TRELLO_USERNAME + "/boards?fields=name&" + TRELLO_CREDENTIALS;
    var res = UrlFetchApp.fetch(url, {'method':'get'});
    
    var boards = JSON.parse(res);
    for (var board of boards) {
      if (board.name === name) {
        return board.id;
      }
    }
    throw new Error("board not found: " + name);
  }
  
  function findTrelloListId(boardId, listName) {
    var url = "https://trello.com/1/boards/" + boardId + "/lists?fields=name&" + TRELLO_CREDENTIALS;

    var res = UrlFetchApp.fetch(url, {'method':'get'});
    var lists = JSON.parse(res);
    for (var list of lists) {
      if (list.name === listName) {
        return list.id;
      }
    }
    throw new Error("list not found: " + listName);
  }
}

スクリプト上部の以下のプレースホルダーは適宜置き換えてください。

  • <USERNAME> → ③の値
  • <TRELLO KEY> → ①の値
  • <TRELLO TOKEN> → ②の値
  • <TRELLO BOARD NAME> → 通知したいListが含まれるBoardの名前
  • <TRELLO LIST NAME> → 通知したいListの名称(僕の場合だとDo Today

これを実行してみます。初めて実行するときには権限を与えてあげる必要があります。

f:id:kenev:20200408161940p:plain

遷移すると下記のような警告が出ます。

f:id:kenev:20200408162346p:plain

AdvancedからGo to Trello to Slack (unsafe) をクリックします(個人でしか使わない前提なのでこの方法にしています)。

SlackにIncoming Webhook経由で通知

Trelloのカードの情報を手に入れたらあとは整形してSlackに通知するだけです。作ったことが無い人は以下の記事を参考にしてみるとイメージがわきやすいと思います。

qiita.com

それではGASを仕上げます。下のコードの<SLACK WEBHOOK URL>には、SlackのIncoming Webhooksで生成されたURLを設定します。

// --------------EDIT REQUIRED START---------------------
// 中略
// Slack
var SLACK_WEBHOOK_URL = "<SLACK WEBHOOK URL>";
// --------------EDIT REQUIRED END---------------------

残りの実装も入れます。

function main() {
  // 中略
  notifySlack(cards);
}
// 中略
function notifySlack(cards) {
  var item = [];
  for (var card of cards) {
    item.push("- <" + card.shortUrl + "|" + card.name + ">");
  }
  var text = item.join("\n");

  // FYI: Message formatting
  // https://api.slack.com/messaging/webhooks#advanced_message_formatting
  var jsonData = {
    blocks: [
      {
        type: "section",
        text: {
          type: "mrkdwn",
          text: "*本日の残タスク*"
        }
      },
      {
        type: "section",
        text: {
          type: "mrkdwn",
          text: text 
        }
      }
    ]
  };
  
  var payload = JSON.stringify(jsonData);

  var options =
  {
    "method" : "post",
    "contentType" : "application/json",
    "payload" : payload
  };

  UrlFetchApp.fetch(SLACK_WEBHOOK_URL, options);
}

これで前半で取得したTrelloのカードをもとにSlack用のメッセージを整形して、通知することができます。実行してみてSlackに通知されたらあと少しで完成です!

GASのTriggerで定期的にスクリプトを実行

それでは、最後にこのGASが自動的に実行されるようにTriggerを設定します。

トリガーの設定画面にメニューから遷移します。

f:id:kenev:20200408171430p:plain

トリガーを追加しましょう。

f:id:kenev:20200408170923p:plain

時間ベースでTriggerが発火するように設定します。

f:id:kenev:20200408175832p:plain:h400

僕は

  • 12〜13時ごろ
  • 16〜17時ごろ
  • 18〜19時ごろ

の3回くらい通知がほしいと思ったので3つ設定しました。

設定後は以下のように3つ表示されます。

f:id:kenev:20200408171712p:plain

完成

以上でGASの設定は完成です!指定した日時にSlackに通知が飛んでくるはずです。これでうっかり「タスクをやり忘れた」ということも改善されることでしょう!

  • Trelloの指定したListにあるカードをSlackに通知するGASを作った
  • 定期的にGASを実行してその日にやるべきタスクを思い出させてくれるようにした

スクリプトの完全版は以下に公開しています。

Trello List to Slack

カスタマイズしましょう

この記事で公開しているスクリプトはかなりシンプルな内容になっています(Trelloカードのタイトルのみ)。実際のAPIではラベルの情報だったり、もっと詳細な情報がたくさんとれますので、自分に合った形でSlackに投稿する内容をカスタマイズしましょう!

フルリモートで1年半働いて個人として工夫していること

f:id:kenev:20200323105639p:plain

フルリモートで働いて1年半ほどが経つので、自分が工夫していることについて残しておこうと思いました。この記事は会社云々とかチームワーク云々とかの話ではありませんのご了承ください。個人として家の環境だったり、習慣に気をつけている点についてまとめます。

ちなみに家族構成としては妻と子供3人(全員小学生未満)を含めた5人です。

【注意】この記事の内容はあくまで我が家で今の所運用できている内容です。各家庭や環境によって効果は大きく異る可能性があるので、その点はご了承ください。

仕事場

Trelloでタスク管理

タスク管理には色々とツールを試しましたが、結局Trelloに落ち着いています。会社で使っているツールがTrello以外の場合は適宜そのツールを使っていますが、公私ともに必ず使っているのはTrelloです。

trello.com

Trelloに最終的になった大きな理由は特にないです。「これだ!」って思えるツールにまだ出会えてないのと、最初に使い始めたのがTrello、というくらいです。

バレットジャーナルで「その日の記録」

Trelloに加えて、その日にやること(やったこと)については、実はツールではなく紙のノートに「バレットジャーナル」というやり方を使って管理しています。やることを箇条書きにして打ち消していくことと、起きた出来事(ミーティングなどの割り込み)も箇条書きにしておくというとてもシンプルなことなのですが、実際にペンでノートに書く影響なのか、頭によく残りますし、翌日も何をやったかがふりかえりやすいです。バレットジャーナルに関しては、それだけで1記事ガッツリ書けるので、紹介だけでとどめます。

バレットジャーナルに関するおすすめ書籍は下記↓

他にも「バレットジャーナル」とググればたくさん情報が出てくるので参考にしてみると良いと思います。

ポモドーロでメリハリ

家でメリハリをつけるのが結構難しいのですが、これにはポモドーロテクニックが非常に役に立っています。25分間集中して、5分休憩をとることもあれば、50分集中して10分休憩することもあります。「ポモドーロ・テクニックについて聞いたことがない!」という方は下の記事を読んでみてください。メリハリを半強制するのに僕は重宝してます!

kakakakakku.hatenablog.com

ポモドーロのアプリも、いくつか使ってみた結果PomoDoneAppに落ち着きました。サクサクニュルニュル動いてくれないという不満がそれなりにありますが、他サービスとの連携が現時点では一番多いのでこれにしています。

pomodoneapp.com

具体的にどのような連携をしているかというと、下のとおりです。

  • タスク管理をTrelloと連携
  • Focus TimeをRescueTimeと連携
  • Do Not DisturbをSlackと連携

一つ一つ軽く紹介します。

タスク管理をTrelloと連携

f:id:kenev:20200319063123p:plain
https://t.ly/qrbep

PomoDoneAppはTrelloと連携してタスクを同期しておくことができます。なので、Trelloでタスク管理して、その中からタスクを選んでポモドーロを行うことができます。

f:id:kenev:20200319063537p:plain:h200
Trelloのタスク

f:id:kenev:20200319063500p:plain:h300
PomoDoneAppで使用

ポモドーロ時間をRescueTimeと連携

RescueTimeについては後述しますが、Focus Timeとポモドーロの開始/終了を連携させることができます。いちいちRescueTime側で開始/終了をやらなくてすむのでかなり助かります。これでポモドーロ中はTwitterYoutubeとかをついつい閲覧しちゃっても強制ブロックされるので安心です!(ブロックしてほしくないコンテンツも例外に追加可能です)

f:id:kenev:20200323063232p:plain
https://www.rescuetime.com/focustime

Do Not DisturbをSlackと連携

集中したいのにかなりの割合でそれの阻害原因となるのが通知です。Slackのようなチャットツールが特に通知が多いと思います。PomoDoneAppはSlackのDo Not DisturbとON/OFFを連携させることができるので、ポモドーロ中は通知が表示されないようにすることができます(自動連携なので、ON/OFFを意識する必要がありません)。

f:id:kenev:20200323064143p:plain
Do Not Disturb

ノイズキャンセリングヘッドホンで音を遮断

リモート勤務で気になるのはです。特に小さな子供がいたりするとこれはかなり深刻な問題。例として:

  • 「あー!!!」と何やら子供が起こした事件に驚く妻
  • 「パパぁ!」と呼びかけてくる子供
  • 猛烈に泣きまくる赤ちゃん
  • 子供がはしゃぎまくってる音
  • ママ友さんが遊びに来てるときの談笑

上げていけばキリが無いのですが、これらは仕事に集中したいときにかなり致命的な障害になります。前提として家族と合意をとる必要がありますが、僕はノイズキャンセリングヘッドホンでこの問題は概ね乗り越えています。使っているのはSONYのWH-1000XM3。

www.sony.jp

これに「Sleep Orbit」のようなアプリで雨音を流しておくことで、家の音はいっさい聞こえなくなります。これで家族も自分もハッピー。(ただし、これを休みの日にやった場合には当然妻は噴火します。あくまで「仕事中は…」という合意をとっておきましょう

Sleep Orbit: Relaxing 3D Sound

Sleep Orbit: Relaxing 3D Sound

  • SMB Studio LLC
  • Health & Fitness
  • Free
apps.apple.com

しかし、結構大きな課題として「ミーティング」などでマイクを使う必要があるときに、雑音がたくさん入ってしまうということが残ります。これに関しては指向性の高いヘッドセットを用意することが一番の解かなと想ってます。以下の記事が非常に参考になりますので、ヘッドセットで悩んでいる方はぜひ読んでみてください。

blog.pirox.dev

家族とSlackで会話

上で紹介しているノイズキャンセリングヘッドホンを使うと、そのままの状態では家族とのコミュニケーションはいっきに難しくなります。うちでは、仕事中であれば僕との会話はSlackで行うようにしてます。イメージ的には、「仕事部屋にいる=出社している=家にはいない」くらいな気持ちを家族には持ってもらっています。なので、妻とのやりとりは家の中に双方いたとしてもSlackで行っていますし、子供たちとはスマートスピーカー経由で会話してます。

子供とスマートスピーカーで会話

テキストメッセージが送れる子供ならともかく、小学校未満の子供とSlackで会話する方法はちょっと工夫が必要です。我が家では

  • 「子供→自分」はIFTTTでGoogle HomeとSlackを連携
  • 「自分→子供」はGoogle HomeのBroadcastでアナウンス

しています。

Google Homeで子供が「OK Google、パパに◯◯か聞いて」と話しかけると、「◯◯」の部分がSlackで通知されるようにIFTTTで連携してます。

ifttt.com

実際にSlackに流れてくる内容は下のような感じです。

f:id:kenev:20200320070412p:plain

一方、僕から子供宛にSlackで返事をするわけにもいかないので、Google Assistantのブロードキャストを使って、音声メッセージをGoogle Homeに流すようにしています。難点としては、Google Homeのそばに子供がいるかどうかわからないので、声が届かないことが時々あります。。。ただし、シンプルなやりとりなら十分機能してます。

RescueTimeでPC上の活動を記録

その日一日PC上で何をしていたのかをふりかえるのに便利なツールがRescueTimeです。

www.rescuetime.com

PCやスマホにソフトを入れておくことでトラッキングしてくれるソフトです。

f:id:kenev:20200321061728p:plain
最も良く使っていたアプリ

自分がどの時間帯に生産性が高かったか、あるいは低かったかがグラフで視覚化されていて便利です。「今日はミーティングばっかりだったな」って思った日はだいたいZoomとSlackが一日を占めています。

ShushでマイクのミュートON/OFFを瞬時に切り替え

オンラインミーティング中、極力家の中の雑音が入らないようにミュートのON/OFFが簡単に切り替えられると便利です。それにぴったりなアプリがShushです。

mizage.com

ショートカットキーに割当てたキーを押すことでミュートのON/OFFが瞬時に切り替えられます。

「オンラインミーティングソフトにショートカットがあるのでは?」という疑問が上がってきますが、仰るとおりで、だいたいの場合はあると思います。ただし、Shushというアプリで一括管理できていると、いちいちアプリに応じてショートカット覚えておく必要ありませんし、Shushがミュート状態であれば全体が必ずミュート状態になるので、心理的安全性が高まると思ってます。

健康

引きこもりがちになるフルリモートにおいて、健康への関心はとても大事です!僕も健康に関してはまだまだ改善していかないといけないのですが、以下の点については工夫をしています。

アーロンチェアで快適なSittingライフ

デスクワークなので椅子は確実に大事です。ということで定番のアーロンチェアを使用しています。

www.hermanmiller.com

10万円以上もするので出費がかなり痛いのですが、10年以上の保証もついてますし、丈夫ですし、何より座り心地が段違いに良いのでコストをかけるだけのメリットがあると思います。

アーロンチェアだけのおかげでは無いと思いますが、フルリモートになってから腰痛になっていないので、大きく貢献してくれていると思います。

課題というか、意図的なのかもしれないですが、あぐらがかけないのがたまにつらいです(笑)無理矢理あぐらをかくこともできますが、足がすぐ痛くなるので長時間は無理です。「そもそも椅子であぐらをかくな」ということだと思うので、かかないように矯正されちゃう方がきっといいですね。

FlexiSpotで半自作昇降式デスク

座りっぱなしな状態は健康に良くないので、立って作業ができるようにデスクをFlexiSpotで昇降式にしました。

ちょうど家の食卓テーブルとして使っていたIKEAのテーブルを買い替えたいと思っていた時期だったので、天板だけ取り外してFlexiSpotに取り付けました。ネジで取り付けるだけなので簡単です。若干天板の強度が心配ですけど、今の所問題なく使えてます。

お腹が空いたらアーモンドかクロレッツ

僕だけなのかもしれませんが、家で作業をしてると何故か異常にお腹が空きます。集中力を削ぎかねないレベルでお腹が空きます。間食していたら運動不足に加えて不健康な食生活というどうしようもない状況になってしまうので、間食はアーモンドとクロレッツに限定するようにしています。

「アーモンドはダイエットに良いらしい」ことと「ミント系ガムは食欲を落とすのと、ガム自体が集中力を高めるらしい」という情報で安易にとびついてます(笑)

f:id:kenev:20200322064500p:plain:h150
素焼きアーモンド

アーモンドも意識していなかったら無限に食べ続けちゃうので、1日24粒(午前12粒と午後12粒)と決めています。

f:id:kenev:20200322064848p:plain:h150
クロレッツ

ガムはだいたいお昼ごはんや晩ごはんを食べる1時間前くらいに噛んで、暴飲暴食をしないようにしてます。

リングフィットで適度な運動

リモートだと運動不足は深刻な問題です。ということでリングフィットを買って家で毎日トレーニングするようにしました。

www.nintendo.co.jp

外に出てジョギングなりした方が良いんでしょうけど、なかなかモチベーションを維持するのが難しいんですよね。。。リングフィットは程よく毎日継続できていて、気のせいか睡眠の質が上がったのと、朝の目覚めもよくなっていると思います。

番外編

その他にもちょっと工夫している点について。

赤ちゃんを背負いながら作業する

休日も含めるとPCと向き合っているときに寝ている赤ちゃんをエルゴで背負うことも珍しくありません。そんなとき、何かしら動いてないとすぐに起きちゃう我が家の赤ちゃんみたいなタイプだと、静止して作業をしているとすぐに起きてギャン泣きが始まります。

意図はしていなかったけれど、そんなときに非常に役に立っているのが昇降式デスクバランスボールです。

昇降式デスクのおかげで横揺れが可能になり、バランスボールのおかげで縦揺れが可能になってます。

f:id:kenev:20200323095618g:plain
昇降式デスクで横揺れ

f:id:kenev:20200323095642g:plain
バランスボールで縦揺れ

(撮影協力してくれた妻に感謝)

最後に

リモート勤務するにあたって工夫している点について色々とリストアップしました。我が家はこれで割とうまく運用できている気がします。ただ、改善すべき点はまだまだ日々感じていて、それらは徐々に良くしていけたらなと思ってます。「セルフマネジメント(自分の管理)」という点もとても大事で、それに関しては以下の記事でも紹介していますので、時間があればあわせて読んでみてください!

kenfdev.hateblo.jp

OPAに関する記事まとめ

以下はこのブログ内のOpen Policy Agentに関する記事を一覧化したものです。

入門

kenfdev.hateblo.jp

kenfdev.hateblo.jp

Regoの実装例

kenfdev.hateblo.jp

kenfdev.hateblo.jp

kenfdev.hateblo.jp

kenfdev.hateblo.jp

Regoのユニットテスト

kenfdev.hateblo.jp

機能の紹介

kenfdev.hateblo.jp

CIと連携

kenfdev.hateblo.jp

kenfdev.hateblo.jp

kenfdev.hateblo.jp

kenfdev.hateblo.jp

kenfdev.hateblo.jp

カンファレンス関連

kenfdev.hateblo.jp

その他

kenfdev.hateblo.jp

アプリケーションにおける権限設計の課題

f:id:kenev:20200113114803p:plain

日々権限設計で頭を抱えてます。この苦悩が終わることは無いと思ってますが、新しい課題にぶつかっていくうちに最初のころの課題を忘れていきそうなので、現時点での自分の中でぐちゃぐちゃになっている情報をまとめようと思い、記事にしました。

所々で「メリット」「デメリット」に関連する情報がありますが、そのときそのときには色々と感じることがあっても、いざ記事にまとめるときに思い出せないものが多々ありました。フィードバックや自分の経験を思い出しながら随時更新する予定です。

TL;DR(長すぎて読みたくない)

  • 権限設計は難しく、ベストプラクティスが広まっていない
  • ネット上に散らばっている情報をかき集めながらBetterなアプローチを追求し続けないといけない
  • 権限設計のノウハウがもっと公に共有されてほしい

本当に長い記事なので目次に気になるトピックがある方だけ続きを読んでください。ところどころ感想文になっていたりする点に関してはあらかじめお詫び申し上げます。

この記事へのフィードバックだったり、僕の継続的な苦悩から生まれる新たな知識をアップデートしていくことで、今後改善させるつもりではあります!

想定する読者や前提知識

  • API設計をしたことがある
  • 権限をどうやって実装するべきか苦しんだことがある
  • ソフトウェアアーキテクチャについて意識している

この記事での権限とは

この記事における権限とは、「誰が(Principal)」、「何に(Resource)」、「何を(Action)」して良い(Allow)のか、あるいはしてはいけない(Deny)のかを定義するものに関して述べています。

例えばInstagramを例にとると、

  • フォローしている人の投稿を参照することができる
  • 自分の投稿を作成することができる(他人の投稿は作成できない)
  • 自分の投稿を削除することができる(他人の投稿は削除できない)
    • 自分の投稿のメニューには「削除」メニューがある(他人の投稿には無い)

のようなものです(かなりシンプルな例を出しました)。

アプリケーションを作る上でほとんどの場合こういう観点の権限が何かしら登場すると思います。

では、この「権限」をどうやって設計・実装するべきなのか?ということがこの記事のメイントピックです。

間違った理解で書いている内容もあるかと思いますが、何かしらの方法でご指摘いただけると幸いです。(その分また一つ権限レベルが上がる気がします)

権限の種類

まず、権限モデルには様々なものがあります。その中でもACL, RBAC, ABACについて軽く紹介したいと思います。

dzone.com

上の記事がわかりやすく説明しているのですが要点だけ言うと以下のとおりです。

ACL(Access Control List)

f:id:kenev:20200106214744p:plain:h100

ACLは「ある対象(よく例に出てくるのはファイルだったりディレクトリだったり)」に対して利用者の何らかのアクセス権限をリスト化したものです。

「勉強会で出席者名簿に名前があれば参加できる」

というように、一覧に入っていればOKというようなシンプルな制御です。

RBAC(Role-Based Access Control)

f:id:kenev:20200106214525p:plain:h150

RBACは、「ある対象(例えばユーザー)」に「ロール(一般ユーザー・管理者など)」という概念が割当てられるようにしたものです。このロールに対して、「何ができるか」というのが付与されるようになります。

「勉強会の一般参加者はGUESTカードを渡され、運営側の人はSTAFFカードが渡される。STAFFカードを持っている人は特定の部屋に入ることができる。」

というように特定のロールに、特定の行動許可が付与されるようなイメージです。

ABAC(Attribute-Based Access Control)

f:id:kenev:20200106214655p:plain:w150

ABACはさらにもう一歩先にいきます。ABACのAがAttribute(属性)であるように、「ある対象の属性」も考慮して「何ができるか」を決めることができます。

「AさんもBさんも勉強会に参加しました。Aさんは20歳なので懇親会でお酒が飲めますが、Bさんは18歳なので懇親会でお酒が飲めません。」

というように、「ある対象(Aさん、Bさん)」の「属性(年齢)」に着目して「何ができるか(お酒が飲めるか)」を決めることができます。

どの権限モデルを採用するべきか

3つの権限モデルを紹介しましたが、他にも色々とあります。どの権限モデルを採用するべきなんでしょう?あるあるですがそれぞれのメリット・デメリットがあるので「It depends」です。上で紹介した中ではABACが最も柔軟ですが、その分実装するのも大変になります。要件がロールだけで完結するアプリケーションであれば、RBACで作る方がシンプルになると思います。ただし、下のようなケースが出てくるとだいたいの場合ABACになっちゃうのかなと思います:

  • 同じグループだったら○○できる
  • 友達だったら○○できる
  • ○○歳以上なら○○できる
  • YYYY年〜ZZZZ年の間に加入した人なら○○できる
  • などなど

要件を見極めながら権限モデルを決める必要があります。

権限を適用する場面

権限モデルについて触れたところで、次はどこで権限を適用するのか、という点について考えます。これには次の3つの観点が重要なのかなと思っています。

  • 機能的な観点
  • 対象範囲の観点
  • 対象項目の観点

タイミング的にはAPIであれば「リクエスト到着時」、「データ取得時」、「データ返却時」に分類できると思っています。下図のそれぞれのポイントになります。

f:id:kenev:20200107183716p:plain

参考までに、Clean Architecture的に言うと下図のポイントに該当します。

f:id:kenev:20200106231020p:plain

それぞれをもう少し詳しくみていきます。

機能的な観点

機能的な観点というのは「そもそも対象となる機能が使えるのか」というかなり広い範囲の観点です。APIであれば、リクエストしてきた人が「何者かわかれば」判断を下すことができます。「一般ユーザーだったら管理者画面を閲覧できない」というようなケースがこれにあたります。

f:id:kenev:20200113002214p:plain

対象範囲の観点

対象範囲の観点というのは、「閲覧」のときであれば「どこまで見ていいか」という意味での範囲です。例えば大きな病院で医者が使うシステムがあって、「患者一覧」を閲覧するAPIがあったとします。このとき、患者はきっとたくさんいますが、全ての患者の情報を全ての医者が問答無用に閲覧できてしまって良いでしょうか?答えはきっとNOです。単純な例だと、きっと医者が担当している患者の情報しか閲覧できない、みたいな制限があるはずです。これは下図のようなイメージになります。

f:id:kenev:20200107225607p:plain

この観点は主にデータを取得する際に適用するものです。

対象項目の観点

対象項目の観点とは、例えば「個人情報のクレジットカード番号は非表示にする」と言うような、「ある対象の具体的な項目」に関する観点です。この観点をどこで適用するのか、というのは未だに僕も悩んでいるのですが、APIであればデータを返却する直前(Presenter層)なのかなと今のところ思っています。

f:id:kenev:20200113003356p:plain:h300
クレジットカード番号を非表示にしている例

参考

3つの観点に関して、似たようなことが以下のStack Overflowでも述べられているので合わせて読んでみると知識が広がります。

stackoverflow.com

権限のソフトウェアアーキテクチャ

何も考えずにアプリケーションの中に権限を実装すると、ビジネスルールとして実装しているのか、権限の関心事として実装しているのか訳がわからなくなってしまいます。(考えながらやっていても訳が分からなくなっているのが僕の現状

権限のアーキテクチャを考える上で参考になったのがXACML(eXtensible Access Control Markup Language)です。XACMLに関しては僕もほとんどわかっていないのですが、「権限制御用のXMLで標準化されている言語仕様」、という認識です。XACMLの詳細については説明することができないのですが、XACMLのアーキテクチャが権限における役割を理解するために非常にわかりやすかったです。以下、Axiomaticsの記事がわかりやすく説明しているのと、動画もあるので英語に抵抗が無い人はぜひ見てみてください。

www.axiomatics.com

記事内に登場する以下の図が特に重要です。

f:id:kenev:20200107225133p:plain:h300
"XACML Reference Architecture" より抜粋

図にあるように、権限のアーキテクチャにおいて重要な役割が4つあります。

  • 権限を管理(Manage)する役割
    • PAP(Policy Administration Point)
  • 権限の判断を下すサポート(Support)をする役割
    • PIP(Policy Information Point)
    • PRP(Policy Retrieval Point)
  • 権限を見て判断を下す(Decide)役割
    • PDP(Policy Decision Point)
  • 権限を適用(Enforce)する役割
    • PEP(Policy Enforcement Point)

この4つの役割を意識することで、権限のアーキテクチャは結構わかりやすく疎結合に組み立てることができます。

僕が理解している範囲でそれぞれの役割をもうちょっと詳しく見てみます。

権限を管理(Manage)する人

f:id:kenev:20200113103312p:plain

ここは読んで字の如く、権限を管理する箇所です。上の図にあるようにPAPからPRPに、権限にまつわる情報が保存されます。AWSで言うところのIAM Management ConsoleもPAPと言えます。

権限の判断を下すサポート(Support)をする人

f:id:kenev:20200113103329p:plain

ここは権限の判断に必要な付加情報を取得する箇所だと僕は思っています。PRPは権限の情報を取得・保持する箇所なので、権限判断で使う情報としてはサポートというよりは核となる部分だと思うのですが、PIPで取得する付加情報が権限の判断を下すサポートをするメインの箇所という認識です。現に、PRPを表現せずに、PIPだけが表現されている図もよく見かけます。ここで言う付加情報とは、例えばAPIのリクエストにはJWTだけが送られてくるけど、そのJWTに紐づくユーザーの属性値(年齢・所属グループ)などのことです。これをPIPが外部リソース(API・DB・LDAPなど)から取得し、権限の判断のためのサポートを行うというわけです。

権限を見て判断を下す(Decide)人

f:id:kenev:20200113103519p:plain

権限の中ではここが一番中心となる部分でしょう。PAPで管理している権限がPRPに保持されていて、PIPからの付加情報と合わせて最終的な判断を下すのがここ(PDP)です。ここが「やっていいですか?」のような質問を受け付ける場所で、「いいよ」あるいは「だめ」という回答をする場所です。

判断の回答は単純ではない

上に「いいよ」と「だめ」といった単純な回答を例にしましたが、実際のところこのような単純な回答だけでは要件を満たせないことが多いと思います。先の例にも使った「医者は担当している患者を閲覧できる」という権限があったとします。PDPがもし「いいよ」と「だめ」しか回答できなかった場合、この要件を満たすためには患者レコードを全部取得して、一つずつに対して「患者Xのデータを閲覧していい?」という質問をPDPにしなければいけません。10,000レコードあれば、10,000回質問する必要があるということです。

これではスケールしません

ではどうすればいいのか?それは、クローズドな質問ではなく、オープンな質問ができるようにする必要があります。クローズドな質問が「患者Xのデータを閲覧していい?」というのに対して、オープンな質問は「どの患者を閲覧していい?」というものです。そして、この回答が「担当している患者であればいいよ」というものになります。つまり、回答は「いいよ」や「だめ」と言った単純なものではなく、もっと複雑なものになります。具体的には、例えばデータストアがRDBだった場合に、回答がSQLそのものである可能性もありえます。

実際にこのソリューションを提供しているAxiomaticsのARQ(Axiomatics Reverse Query)が以下の動画で紹介されていて、非常に興味深いです。

www.youtube.com

また、下でも紹介しますが、Open Policy Agentの以下の記事に関しても似たようなことを実現しようとしていて興味深いです。

blog.openpolicyagent.org

この観点に関して、言っていることはわかったとしても「じゃあ実際どう実装するの?」という点に関しては結局僕も答えが見つかっておらず、模索中です。少なくともAxiomaticsのようなソリューションを自分たちで作るのは非現実的だと思います(Axiomaticsはそれを専門にビジネスにしている会社なので)。また、そもそも「回答をSQLにする」というのも、権限の内容とデータベースの項目が密結合すぎて繊細になりすぎるのでは、とも思っています。

権限を適用(Enforce)する人

f:id:kenev:20200113103551p:plain

最後に重要なのが、権限の質問をして回答をもらった後に権限を適用する箇所で、それが上図でいうPEPとなります。APIであれば、「いいよ」という回答がもらえたら処理を続行し、「だめ」と言われたら403 Forbiddenを返す、といった処理が「権限を適用」していることになります。

権限実装のアプローチ

実際に権限はどうやって実装すると良いのか、というのもまた悩みの種です。「対象範囲の観点」で権限を適用するときを例にとって、実装のアプローチについて挙げてみます。

ハードコードするアプローチ

権限実装においてハードコードと言われて何のことなのかイメージできるでしょうか?僕は最初は全くイメージできていなかったです。僕の中での当初のハードコードというのは下のようなコードです。

// '100'って書いちゃってることがハードコードだと思っていた
if (employee_id === '100') {
  execute();
}

このイメージが一般的には多いんじゃないかと思います。しかし、権限実装におけるハードコードというのは少し毛色が異なります。現状の僕の理解は以下のとおりとなっています。

権限実装における「ハードコードしている状態」とは、権限の内容が変わった場合に、権限の関心以外のコードが影響を受けてしまう状態

ふわっとしててわかりづらいと思うのですが、具体例をあげます。先に述べた患者一覧を取得するAPIについて考えてみます。「患者一覧を取得する」APIであれば、単純に考えたらエンドポイントは /api/patients のようなものになるのが想像できると思います。そしてこのリソースをDBから取得する場合のコードはきっと下のようになります(JSっぽい擬似コードです)。

function findAllPatients() {
  return query.execute('SELECT * FROM patients');
}

ただし、これでは本当に「全患者を取得」してしまいます。「見て良い範囲の権限」が適用されていません。先の例では「担当している患者だけ閲覧して良い」という要件があるので、この権限をコードに適用すると下のようになります。

// doctorIdにはリクエストした医者のIDが入る
function findAllPatients(doctorId) {
  return query.execute(
    'SELECT * FROM patients p WHERE p.doctor_id = ?',
    doctorId);
}

この WHERE p.doctor_id = ? と書いた場所が権限実装における「ハードコードしている箇所」になります。一見 ? には動的に医者のIDが入るので、ハードコードしているようには見えないのですが、権限の内容が変わった場合のことを考えてみましょう。例えば、「担当している患者だけ閲覧して良い」という権限から「同じ診療科内の医者が担当している患者だけ閲覧して良い」に変わったとします。こうなると、下のようなコードに変える必要がありそうです。

// doctorIdにはリクエストした医者のIDが入る
function findAllPatients(doctorId) {
  // 医者の所属診療科を知りたいので医者の情報を取得する
  doctor = query.execute(
    'SELECT * FROM doctors d WHERE d.id=?',
    doctorId);
  // 医者の所属する診療科内の医者ID一覧を取得する
  doctorIds = query.execute(
    'SELECT id FROM doctors d WHERE d.department_id=?',
    doctor.departmentId);
  // 患者の担当医が上で取得した医者IDと一致するものだけ取得する
  return query.execute(
    `SELECT * FROM patients p
      WHERE p.doctor_id IN (${doctorIds.join(',')})`);
}

これが権限を「ハードコード」してしまっているがために、本来影響を受けてほしくないfindAllPatientsの実装に変更が発生してしまっている状態です。患者一覧を取得する関数(findAllPatients)のはずが、権限の関心が紛れているせいで権限の内容が変わった場合に影響を受けてしまう、というものです。

「いや、要件が変わってるんだから影響受けてもいいでしょ」

と、思うかもしれませんが、これが医者によって閲覧できる範囲が変わったりしたらどうなるでしょう?例えば「一般権限の医者は担当している患者を閲覧できる」が、「マネージャー権限をもつ医者は同じ診療科内の医者が担当している患者を閲覧できる」といった違いがある場合です。別々の関数を用意して、マネージャーなのか、そうじゃないのかで使い分けたりしそうな複雑な実装になりそうです。

function findAllPatients(doctor) {
    patients = [];
    if (doctor.role === 'MANAGER') {
        // マネージャーだったら
        patients = findMyDepartmentsPatients(doctor.id);
    } else {
        // マネージャーじゃなかったら
        patients = findMyPatients(doctor.id);
    }

    return patients;
}

// 担当している患者を取得
function findMyPatients(doctorId) {
  // ...
}

// 同じ診療科内の医者が担当している患者を取得する
function findMyDepartmentsPatients(doctorId) {
  // ...
}

ユースケースとしては「患者一覧タブを表示したら患者一覧を取得する」というシンプルそうに聞こえるものなはずなのですが、findAllPatients関数の中身を見たら「なんだか思ったより複雑なことをしている」という状態になっているのが想像できると思います。

f:id:kenev:20200110235602p:plain:h200
患者一覧を表示する画面のイメージ(ユースケースはシンプルに見える)

コアロジックから切り離すアプローチ

ということでハードコードのアプローチは小規模〜中規模なアプリケーションまでなら問題無い気がしますが、中規模〜大規模なアプリケーションになってくるとコードの可読性がしんどくなってくるかと思います。そこで、権限の関心の分離をする方法がもっとスケールするのかなと思います。

このアプローチには上の例で示しているような「生のSQLを書く」やり方は厳しいんじゃないかと現状の僕は思っています。SQL Query Builderのような、生のSQLをWrapするようなものがあって、SQL実行前に介入しつつ、権限を適用する仕組みが必要だと思っています。

僕は身近でLaravelを使っているので、LaravelのQuery Builderの力を借りると下のコードのように権限とコアロジックの分離ができそうです。患者を取得するORM(Eloquent Model)をPatientとすると:

<?php

// ...

// 患者一覧を取得
$patients = App\Patient::enforcePolicy()->get();

enforcePolicyというのがLaravelのScopeを使っていて、モデル内では下のように実装されています(「担当している患者」に絞り込んでいます)。権限の内容に変更があった場合は、下のscopeEnforcePolicyの中身を変えるだけで済み、コアロジック側には影響が無いという状態にすることができます。

<?php

namespace App;

use Illuminate\Database\Eloquent\Model;

class Patient extends Model
{
    /**
     * @param  \Illuminate\Database\Eloquent\Builder  $query
     * @param  String  $doctorId
     * @return \Illuminate\Database\Eloquent\Builder
     */
    public function scopeEnforcePolicy($query, $doctorId)
    {
        // 担当している患者だけに絞り込む
        return $query->where('doctor_id', $doctorId);
    }
}

ただし、現時点ですでに課題と感じている点としては

  • App\Patient::enforcePolicy()->get()enforcePolicy()を呼び出している時点で明示的にORMを使う側が権限を意識しないといけないので、結構繊細なコードになってしまう(enforcePolicy呼び出し忘れたら全取得できてしまうセキュリティリスクがある)
  • ↑に似てますが、PatientモデルにscopeEnforcePolicyという実装を入れているので、Patientが権限を意識する必要がある。さらに隠蔽化、あるいは処理を移譲してPatientが意識しないようにするべき

ということです。フレームワークレベルでさらにいい感じに実装できるものがありそうですが、現状では僕はそこまで把握できておらず、イメージ的にこのような分離ができたら素敵なのでは、と妄想しているだけです

権限はビジネスルールの関心事なのか

上で色々と言っておきながらそもそも論な疑問になってしまうのですが、Clean Architectureだったり、ドメイン駆動設計にも挑戦している中、「権限はビジネスルールの関心事なのか」という疑問へのスッキリする答えが未だに見つかっていません。少なくとも上に述べている実装のアプローチは、ビジネスルールと権限の関心を分離しているアプローチのつもりです。なので、権限の変更によってビジネスルール側(ドメイン)には影響が無いようにしています。

Clean Architectureのボブおじさんも、Clean Code Discussionにおいて以下のように発言しています。

Security is an application specific concern, it belongs to the interactors. The controllers would access the current user's credentials and pass that information to the interactors. The interactors would use an authorization service to ensure that their particular interaction was authorized. The business objects wouldn't know anything about it.

「セキュリティはアプリケーション特有の関心事であり、ビジネスオブジェクトはこのことについて意識しない」と言っています。なのでこの言葉をそのまま鵜呑みにすると、「権限の関心をビジネスルール内に持ってこない」考え方は間違っていないのかなと思っています(セキュリティに関連するアプリケーションを作っているのであれば話はきっと違う)。

この観点に関しては色々な人の色々なアプリケーションでの色々な実装方法を聞きたいです。

システムアーキテクチャ

どこまで権限の関心を分離するのか、と考えたときに意識しないといけないのがシステムアーキテクチャです。モノリシックとマイクロサービスなアプローチについて言及し、それぞれのメリット・デメリットについて述べたいと思います。

モノリシックなアーキテクチャ

迷った場合にまずとってみるアプローチがモノリシックなアーキテクチャかと思います。下図のように、上に出てきたPEP, PDP, PIP, PAPをすべて同じアプリケーションに詰め込むことになります。

f:id:kenev:20200111235737p:plain:h300

それぞれの○が別々のアプリケーションサービスになっていくのがわかりやすい分け方かなと思っています。

考えられるメリット

  • 一つにまとまっているので管理がしやすい(はず)
    • 一つのコードベースなのでコードが追いやすい
    • CI/CDなど開発プロセスもシンプル

考えられるデメリット

  • プログラミング言語の縛りが発生し、言語に依存したライブラリしか使えないことになる
  • 権限に変更が発生すれば基本的にアプリケーションをデプロイし直す必要がある
  • 他のアプリケーションが同じ権限基盤を使いたい場合に、不必要なアプリケーション間の依存ができてしまう

参考

権限の実装をするにあたってスクラッチから全部作るのは大変なので、Casbinのようなライブラリの力を借りながら実装していけるのかなと思っています。

casbin.org

マイクロサービスなアーキテクチャ

f:id:kenev:20200112230522p:plain:h300

運用上の懸念点が増える一方で、最も権限の関心の分離が行えるのがこのマイクロサービス的アプローチになるかと思います。PEP以外(PDP, PIP, PAP)の権限に関連する登場人物はすべて別アプリケーションにします。

考えられるメリット

  • 権限アプリケーションに最適な言語・ライブラリを選択できる
  • 権限アプリケーションとその他アプリケーションのデプロイストラテジを分離できる
  • 権限アプリケーション単体でスケールできる

考えられるデメリット

  • 全体的に管理が複雑になる
    • コードベースの分離
    • ソフトウェアアーキテクチャが複雑になりがち
    • 別アプリケーションであるがための懸念事項の増加(特にネットワーク周り)

参考

Open Policy Agent

CNCFのプロジェクトでもあるOpen Policy Agentがかなり興味深い存在です。Open Policy Agentにはサーバーモードがあるので、Out of the boxでそのまま別アプリケーションとして使うことができます。そのままの機能では不十分だと感じたら、Goのライブラリとしても使うことができるので、強力な権限アプリケーションをGoで自作することも可能です。

www.openpolicyagent.org

また、権限用の独自の言語Regoを扱っていて、権限を宣言的に定義することができます。Open Policy AgentやRegoに関してはこのブログでも何度か紹介しているので、気になる方はぜひ読んでみてください。

kenfdev.hateblo.jp

Authzforce

僕は使ったことがないのですがXACMLに準拠していてOSSなもので名が通っているものとしてAuthzforceというものがあるようです。Community Editionもあるみたいなので、評価してみる価値があると思います。

authzforce-ce-fiware.readthedocs.io

Axiomatics

有償にはなりますが、Axiomaticsのソリューションを使うという選択肢もありそうです。XACMLのラッパー言語としてALFAを使うことができ、宣言的に権限を定義することができます。Kubernetes上にもデプロイできるようで、PayPalも使っているようです。

www.axiomatics.com

余談ですが、権限周りのStackOverflowの質問に対して、ものすごくわかりやすくて良い回答をしている人がいると思った場合にユーザーを見てみると、だいたいAxiomaticsのDavid Brossard氏です。権限の知識を増やしたければフォローしておくとすごく勉強になります。

stackoverflow.com

モダンなフロントエンドにおける懸念事項

忘れがちなのがフロントエンドの観点です。SPA(Single Page Application)登場前であればフロントの生成(HTML)もサーバーサイドが担当していたので、サーバーサイドが持っている権限の情報をフロント側にも使えば良いというものでした。

LaravelのPolicyをBladeテンプレートで使う例がこのやりかたに当たります。他のフレームワークでも似たようなことができるはずです。

@can('update', $post)
    <!-- The Current User Can Update The Post -->
@elsecan('create', App\Post::class)
    <!-- The Current User Can Create New Post -->
@endcan

@cannot('update', $post)
    <!-- The Current User Can't Update The Post -->
@elsecannot('create', App\Post::class)
    <!-- The Current User Can't Create New Post -->
@endcannot

laravel.com

しかし、最近はReact, Vue.js, AngularなどのSPAを使うことが増えてきたため、フロントとサーバーサイドが分離しています。ということは、権限情報は標準でフロント側は持っていないということを意識しておく必要があります。やり方としては大きくは2つあるのかなと思っています。下図のような「編集機能付き患者一覧画面」を例にとって、どういうアプローチがあるか見てみます。

f:id:kenev:20200112113704p:plain
編集機能付き患者一覧画面

リソース自体に権限の内容を埋め込む方法

この方法が一般的には多い印象があります。APIのレスポンスが以下のような形になります。

[
    {
        "id": "1",
        "name": "患者A",
        "doctorName": "医者A",
        "editable": true
    },
    {
        "id": "2",
        "name": "患者B",
        "doctorName": "医者B",
        "editable": false
    },
    {
        "id": "3",
        "name": "患者C",
        "doctorName": "医者A",
        "editable": true
    }
]

ここに登場するeditableのキーを「権限情報がリソースに埋め込まれてる状態」と僕は呼んでいます。「患者」のリソースを取得できるAPIのはずが、患者とは関係ない「編集可能かどうか(editable)」という権限の情報もまぎれこんでいるのがわかります。

考えられるメリット

  • フロントは圧倒的に実装しやすい(上の例であればeditableを見るだけで編集して良いかどうかが判断できる)

考えられるデメリット

  • リソースに権限の情報が紛れ込んでしまう
  • ↑の影響でサーバーサイドの「患者一覧取得」APIは、患者の情報以外に権限の情報をJSONに付加する必要がある

権限情報をリソースと切り離す方法

この方法は馴染みがあまり無いという人が多いのではないかと思います。このアプローチの場合、APIから取得するリソースの情報に原則として「権限情報」を含めません。権限情報はAPIを用意することになります。

まず、患者一覧を取得するAPIJSONは下のようになります。

[
    {
        "id": "1",
        "name": "患者A",
        "doctorName": "医者A"
    },
    {
        "id": "2",
        "name": "患者B",
        "doctorName": "医者B"
    },
    {
        "id": "3",
        "name": "患者C",
        "doctorName": "医者A"
    }
]

前の例との違いは、権限情報であるeditableが消えたことです。では編集可能な患者はどのように見分けるのでしょう?そのために別途権限情報を取得するAPIを用意します。権限のAPIに対しては「私は何ができますか?」という問い合わせをするイメージです。以下は権限APIのレスポンスJSONの一例です。

[
  {
      "action": "read",
      "resource": "patient"
  },
  {
      "action": "edit",
      "resource": "patient",
      "condition": {
          "doctorName": "医者A"
      }
  }
]

読み替えると、「すべてのpatientリソースの閲覧ができる」ことと「doctorName医者Aとなっているpatientリソースなら編集できる」、という内容になります。

下図のように、2つのAPIから取得した情報を組み合わせることでUIを作ることになります。

f:id:kenev:20200112220119p:plain

考えられるメリット

  • API設計時に、リソースに対する権限の関心を基本的には考えなくて良くなる
    • 例:患者取得APIで、レスポンスのJSONeditableの値に何を入れるか気にしなくて良い
  • 関心の分離のおかげでコードの保守性が高まる(はず)

考えられるデメリット

  • 良くも悪くも、権限の関心が分離していることによる設計のコストだったり学習コストはかなり高くなる
  • フロントの実装コストが高くなる(権限APIと、対象となるリソースを組み合わせてUIの挙動を考える必要がある)

参考

権限の分離を行っているアーキテクチャを支援してくれるライブラリだったり、ドキュメントがいくつかあるので紹介します。

CASL

CASLというライブラリが主要なJSフレームワークと連携して使うことができます。

stalniy.github.io

何に対してどんなアクションを、どんな時に実施して良いかという権限の関心を独立して管理することができます。参考まで、以下が実際のJSONでの定義例です。

[
  {
    "actions": ["create", "read", "update", "delete"],
    "subject": "Post",
    "conditions": {
      "author": "1"
    }
  },
  {
    "actions": ["read", "update"],
    "subject": "User",
    "conditions": {
      "id": "1"
    }
  }
]

1つ目の定義は「Postのauthorの値が"1"であれば、create, read, update, deleteのアクションを許可する」というもので、2つ目の定義は「Userのid"1"であれば、read, updateのアクションを許可する」というものです。この情報をUI側から使って、例えば「編集ボタンを表示・非表示」にすることができます。

Chef Automate

このトピックについて説明しているドキュメントとしてChef AutomateのAuthorizationコンポーネントがあります。特にIntrospectionの節がこれにあたります。権限設計含めて神がかっているドキュメントだと思うので、ぜひ一度読んでみることをおすすめします。

github.com

権限設計はどこを目指したら良いのか

ロバストな権限設計を!」と言われても「権限実装 ベストプラクティス」とググったところで「権限」のカバーしている領域が広すぎて求めている情報がなかなか見つかりません。その中で僕が今の所目指している方向性としては「AWSのIAMのような仕組み」です。

理由としては:

  • 上にも何度が言及しているOpen Policy AgentのSlackで度々「AWSのIAMのような仕組みを作りたい」って人が登場する
  • Open Policy Agentを使っているChef Automateも方向性が似ている(ただしConditionという概念はまだ無い)
  • Open Policy Agentを使っているory/ketoの仕組みもAWSのIAMに似ている(Conditionの概念もある)
  • IAM Policyの共通のJSONフォーマットがわかりやすいし、先人の知恵がつまっている
  • AWSのIAMに関するドキュメントが権限設計においてすごく勉強になる

というように、権限設計・実装をするためのとっかかりがあった、という感じです。ただ、僕自身がOpen Policy Agentにはもともと期待しているというバイアスが働いてるのも理由としては大きいと思います。

完全に「AWSのIAM」を作るところを目指すのは非現実的ですが、現実的なラインを見極めながらシンプルで良いところを自分のシステムに組み込んでいくアプローチが良いんじゃないかと思っています。

本当に強力な権限サービスが必要であれば、Axiomaticsのような有償サービスに移譲することも有用な選択肢の一つだと思います。これに関してはコストとユースケースのフィットを自分なりに見極めるしかないでしょう。

最後に

ここまで読んでいただきありがとうございます。そして、まとまりの悪い内容になっていることを再度お詫びいたします。ただ、同じく権限設計に悩む誰かのヒントになったり、同じように悩んでいる人がいるんだよという共感を持てる内容にどこかしらなっていたら幸いです。

今後もこの記事をアップデートしていくつもりですが、独りで苦悩していてもつらいので、権限設計Meetupとかあったら面白いんじゃないかな、と思いました。色々な人の泥臭い話が聞ける気しかしないです。また、「Architecting Authorization」みたいな本が出てほしいなという気持ちもあります。本当に。。。世の中のエンジニアはどうやってこの苦悩と闘っているのでしょう?

今の僕はというと、Open Policy AgentのSlackを見ながら、同じように苦悩している人の会話を日々追っています。興味がある人はぜひJoinしてみてください。

slack.openpolicyagent.org

長々と書いてしまいましたが、いったんここまでとして引き続き精進し続けたいと思います。

その他参考情報

AWS IAMの「ポリシーの評価論理」に関するドキュメント

https://docs.aws.amazon.com/ja_jp/IAM/latest/UserGuide/reference_policies_evaluation-logic.html

AWS IAMに関する丁寧な解説と、最後にはABACの例も紹介されている動画

www.youtube.com

AxiomaticsがRBACからABACへのパラダイムシフトについて詳しく述べている記事

www.axiomatics.com

IAMに関する概要説明をAxiomaticsのDavidが回答しているStack Exchange

security.stackexchange.com

ABACの実装についてAxiomaticsのDavidが説明している記事

www.axiomatics.com

AxiomaticsがDynamic FilteringとDynamic Maskingについて述べられている記事

www.axiomatics.com

AxiomaticsがDynamic AuthorizationをDemoとともに丁寧に解説している動画

www.youtube.com

ConftestとCircleCIで継続的に構造データにPolicyを適用する

構造化されたデータにPolicyを適用したい、って気持ちはチームでコラボレーションしていたら自然と生まれてくる欲求だと思います。コードの場合は最近だと優秀なLinterがあるおかげである程度は強制することができるのですが、例えばYAMLだったりiniファイルだったりはどうでしょう?これらの構造化されたデータに対して、自分で作ったPolicyを適用できたら素敵だと思いませんか?そこで取り上げたいのがConftestです。以前このブログでも紹介したことがあるので、初めて聞いたという方はこちらの記事も合わせて読んでみてください。

kenfdev.hateblo.jp

今回はConftestをただ単体で使うのではなく、CircleCIと組み合わせて継続的にPolicyを適用する方法について見ていきます。これを実現したいがためにconftest-orbというCircleCIのOrbを作りました。

github.com

概要

CircleCIでconftest-orbを使った最小限のYAMLは以下のようになります。

version: 2.1
orbs:
  conftest: kenfdev/conftest-orb@x.y # バージョンは指定してください
workflows:
  build:
    jobs:
      - conftest/test:
          pre-steps:
            - checkout
          file: config_to_test.yaml

前提条件は以下の通りです:

これらを守った上でconfig_to_test.yamlに対してPolicyを適用します。

Serverless FrameworkのYAMLに適用してみる

では、もうちょっと具体例をあげてみます。Serverless FrameworkのYAMLを例にとって、Policyを実際に適用してみます。以下のリポジトリに実際にソースを置いています。

github.com

.circleci/config.ymlに設定しているCIの内容は以下のとおり:

version: 2.1
orbs:
  conftest: kenfdev/conftest-orb@0.0.8
workflows:
  build:
    jobs:
      - conftest/test:
          pre-steps:
            - checkout
          file: serverless.yaml

リポジトリの構成も、上で述べている前提条件を守っています。

kenfdev/conftest-serverless-circleci
├── policy
│   ├── base.rego
│   └── util.rego
└── serverless.yaml

そして、Policyを適用する対象となるserverless.yamlは以下のような内容になっています。

service: aws-python-scheduled-cron

frameworkVersion: '>=1.2.0 <2.0.0'

provider:
  name: aws
  runtime: python2.7
  tags:
    author: 'this field is required'

functions:
  cron:
    handler: handler.run
    runtime: python2.7
    events:
      - schedule: cron(0/2 * ? * MON-FRI *)

今回適用するPolicyの詳細(Regoファイル)についてはこの記事では見ません。興味がある方は実際に見てみてください。概要としては次のとおりです。

  • provider.tagsauthorを設定していること
  • provider.runtimeにPython2.7を設定していないこと
  • functionsruntimeにPython2.7を設定していないこと

上のYAMLと照らし合わせて、最初のPolicyは守られているけど、最後の2つに関しては守られていないことがわかります。この状態でCircleCIを走らせると以下のようにちゃんと叱られます。

f:id:kenev:20191218175806p:plain

Policyを一箇所で管理する

ここまでで、いい感じにCIでPolicyを適用できることがわかりました。ただ、結構大きな問題点があります。それは、適用したい構造データと同じリポジトリにPolicyも管理している点です。Policyは再利用するケースが多いと思うので、上で用意したようなPolicyをすべてのリポジトリで管理なんてしていられません。ということでここでConftestの強力な機能であるPolicyのpushとpull機能を紹介します。

v0.15.0時点でpush先、あるいはpull先として使えるのはOCIレジストリになります。Docker Registryが身近で使えるOCI互換のレジストリなので、これをどうにか使えないか試行錯誤してみました。

かなり斜め上な使い方をすることになってしまいますが、GitHub上にPolicyを保存して、更新するたびにこのPolicyを含んだDocker Registryのコンテナイメージを作るようにしてみました。流れのイメージとしては下図のとおりとなります。

f:id:kenev:20191218180955p:plain

この仕組みを使ってCircleCIのOrbに関するベストプラクティスが適用されているか、継続的にチェックしたいと思います。Policyは以下のリポジトリで管理しています。

github.com

Policyを含んだDocker Registryがあることで、CircleCI上でConftestを実行する際に一箇所で管理されているPolicyを再利用して適用することができます。

conftest-orbの開発フローで面白い点は、自分自身のCIにCircleCI Orbのベストプラクティスを継続的に適用していることです。ドッグフーディング状態だと言えます。

そんなconftest-orbのCIは以下のように定義しています。

jobs:
  general_usecase_test:
    executor: machine
    steps:
      - checkout
      - circleci-cli/install
      - run:
          name: Pack the orb.yml
          command: circleci config pack src > orb.yml
      - conftest/install
      # start the OCI registry(this command is declared in a different place)
      - start_oci_registry:
          image: kenfdev/circleci-orbs-policies
      # pull the policies from the OCI registry
      - conftest/pull:
          policy_path: policy
          repository: 127.0.0.1:5000/policies:latest
      # test with minimum options
      - conftest/test:
          policy_path: policy
          file: orb.yml

ちょっとグチャっとしている点もありますが、それはCI中にDocker Registryを起動する必要があるからです。外部にすでにOCI Registryがあれば、下のようにすっきり書くことができます。

version: 2.1
orbs:
  conftest: kenfdev/conftest-orb@0.0.8
workflows:
  build:
    jobs:
      - conftest/test:
          pre-steps:
            - checkout
          repository: <path-to-your-oci-registry>
          file: serverless.yaml

<path-to-your-oci-registry>からPolicyをpullしてConftestを実行することができます。

このように、Policyを一箇所で管理しつつ、複数のリポジトリに対してCI経由で継続的に適用するということが比較的観点に実現することができます。Conftestのexampleを見ると、下の一覧のようにかなり様々なユースケースに適用できるということがわかるので、興味がある方はぜひconftest-orbも使ってみてください!

  • awssam
  • compose
  • configfile
  • docker
  • hcl2
  • ini
  • kubernetes
  • serverless
  • tekton
  • terraform
  • ts
  • xml
  • etc.

まとめ

  • conftest-orbを使ってCircleCIで継続的に構造データにPolicyを適用できる
  • OCI Registryを使ってPolicyを一箇所で管理して共有することができる

以下のPRもマージされたため、v0.16.0以降ではOCI Registryではなく、HTTPでPolicyを外部から取得できるようになるので、さらに簡単にPolicyを適用できるようになりそうです!

github.com

関連

kenfdev.hateblo.jp