Slackを使ってバグ管理ィィィィィ

こんにちは。ジョジョ4部アニメが開始して毎週金曜が楽しみでたまらないおいかわです。

ある自社サービスのリリース直前なのですが、現在最後のバグ出し合宿をしているところです。そこでやっているバグ管理について紹介します。

今回のバグ管理のポイント

  • 通常弊社ではBacklogを使っているが、もっとさくっと簡単にやりたい
  • スクショを簡単に貼り付けたい
  • スピード重視で管理と情報共有を同時にしたい

で、思いついたのがコレ。

Slackを使って簡易的なバグ管理をする!

以下説明します。

ルール

  • バグ管理用のチャネルを1つ作成
  • バグを上げる場合はどんなものでもスクショをとって画像で上げる(スレッド形式でコメントできるから)
  • 画像のタイトルでバグの名前をざっくり書く、またタイトルの先頭に【機能名】をつける
  • バグを上げたらピンでとめる
  • 基本的にはスクショのコメントでバグのやり取りをする
  • バグ修正後にテストサイトに上げたらピンを外す

バグの起票とそのバグに対するやり取りはこんな感じ

アップロードされた画像のコメント一覧。画像アップロード=バグ起票となり、そのバグの修正に関するやり取りがスレッド形式で行われます。

f:id:jabe20:20160514195337p:plain:w300

バグのリストはこんな感じ

バグ管理用チャネルのピン一覧、ピンの数が残バグ数。タイトルが機能名で整理されているのでけっこう見やすい。

f:id:jabe20:20160514195656p:plain:w300

ピンを外すと

起票者(ピンでとめた人)のところに通知が来ます。通知が来たら再テストすれば良いので、そのコミュニケーションもSlackでリアルタイムに実現!

f:id:jabe20:20160514200005p:plain:w300

Slackでバグ管理をしてみた感想

良い!!!

最初に挙げたポイントを全てクリアし、ものすごく簡単にバグ管理ができました。特に管理と情報共有のスピードが圧倒的。さらに、gitとも連携しているので、バグ管理と構成管理の情報が全てSlackで見れます。

プロジェクトによってバグ管理に求められるものって変わってきますよね。過去のバグの分析とかが求められる場合もあったりしますが、そういう場合にはもちろんSlackでのバグ管理は向いていません。プロジェクトの性質によって、効率の良いやり方をこれからもどんどん見つけていきたいですね。

AWS LambdaからGoogle APIを呼び出す

こんにちは、AWS担当のwakです。間が空いてしまったので、今回は簡単な記事を書いて隙間を埋めることにします。

f:id:nurenezumi:20160428191057j:plain

背景

世はSlackが大流行ですが、未だにエラー通知メールという仕組みも残っており、これを無視するわけにはいきません。そこで、Gmail(Google Apps)に届いたエラーメールを担当者がいるSlackのチャンネルに流す連携を作りました。今回はGoogle APIを呼ぶための仕組みと、AWS Lambdaから呼び出す手順について説明します。

何をするか

次のことをやります。

  1. Google APIを呼び出すアプリケーションを作り、ユーザーのGmailの情報を読み取れるようにする
  2. 自分のユーザーGoogleログイン→OAuth認証を行い、トークンを取得する
  3. トークンはDynamoDBに入れておく
  4. トークンは1時間で有効期限が切れてしまうため、Lambdaで自動的に更新する
  5. このトークンを使って何かする

プロジェクトの準備

プロジェクト作成

まずGoogle Developers Consoleにログインし、画面右上のメニューから「プロジェクトの作成」を行います。「概要」>「Google API」から、このアプリケーションが呼び出したいAPIを選択します。ここではGmail APIを選択しました(複数選べます)。

f:id:nurenezumi:20160426174114p:plain

APIが選択できたら「認証情報」から「認証情報を作成」をクリックし、「OAuthクライアントID」を選択しましょう。

f:id:nurenezumi:20160426174046p:plain

OAuth2.0のおさらい

ここで簡単にOAuth2.0のおさらいをします。一般にWebサービスが提供するOAuth認証を利用するアプリケーションは、

  • ユーザーが許可した権限でWebサービスAPIを利用することができる
  • APIを呼び出すときには、認証時に発行されたキーをリクエストに含める(ユーザーのIDやパスワードは分からない)
  • このキーには有効な寿命が設定されていることがある
  • 有効期限がある場合、認証時に与えられたリフレッシュトークンを使ってリクエスト時に更新しなければならない
    • 定期的に更新しても構わない

という流れなのでした(そしてGoogleOAuth認証では寿命が設定されています)。これを念頭に次のステップへ進みます。

認証画面設定

OAuthクライアントIDを作るためにはまず「同意画面」の設定が必要となります。これはユーザーに「このアプリケーションに権限を与えますか?」という確認を提示するための画面で、ユーザーがその可否を判断するために必要な連絡先やアプリケーション名を記入しておきます。

f:id:nurenezumi:20160426174024p:plain

最後に「アプリケーションの種類」として「その他」を選んで「作成」をクリックすれば完了です。その場でクライアントIDとクライアントシークレットが発行されます。これは後からでも確認できるのでメモを取る必要はありません。

f:id:nurenezumi:20160426174413p:plain

認証&トークン取得

初回認証

認証に使うアカウントでGoogleにログインした状態で、次のURLをブラウザから開きます(改行と空白は削除して1行にしてください)。

https://accounts.google.com/o/oauth2/v2/auth
  ?scope=https://www.googleapis.com/auth/gmail.readonly%20https://www.googleapis.com/auth/gmail.labels
  &redirect_uri=urn:ietf:wg:oauth:2.0:oob
  &response_type=code
  &client_id=999999999999-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx.apps.googleusercontent.com
  • scopeにはこのアプリケーションが要求するスコープ(下述)をスペース(%20)区切りで並べます。
  • redirect_urlは固定値です。ここでは認証後に外部へは遷移せず、認証コードを表示する画面を表示することを指示しています。詳しくはドキュメントを参照してください。
  • client_idには最初に取得したクライアントIDを書きます。
  • approval_prompt, access_typeを指定すると書いてある記事も多くありますが、現時点では必要ないようです。

スコープは様々な権限を適当な粒度で丸めたものです。たとえばメールを読み取る権限が必要なだけならhttps://www.googleapis.com/auth/gmail.readonlyと書けば良いのですが、メールにラベルを追加・削除したければhttps://www.googleapis.com/auth/gmail.labelsも指定しなければなりません。Gmailについてのスコープはドキュメントに一覧があります。

このURLを開くと、画面には権限のリクエストを許可するか拒否するかの選択肢が表示されます。

f:id:nurenezumi:20160426191729p:plain

要求されている権限は確かにスコープで指定したものであることが分かります。ここで「許可」を押すと秘密のコードが表示されます(これはWebページのタイトルにも出力されています)。このコードこそが、ユーザーがアプリケーションに権限を与えたという証拠になります。

f:id:nurenezumi:20160426175521p:plain

トークン2種類を取得

このコードを含めて再度リクエストを送るとaccess tokenとrefresh tokenが取得できます。curlで次のコマンドを叩きます。

curl -d client_id=999999999999-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx.apps.googleusercontent.com \
     -d client_secret=xxxxxxxxxxxxxxxxxxxxxxxx \
     -d grant_type=authorization_code \
     -d redirect_uri=urn:ietf:wg:oauth:2.0:oob \
     -d code=XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX \
     https://accounts.google.com/o/oauth2/token

client_id, client_secretはプロジェクト作成時に貰った値、codeは今ブラウザに表示された値です。うまく行けばJSON形式で結果が返ります。

{
  "access_token" : "xxxx.xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
  "token_type" : "Bearer",
  "expires_in" : 3600,
  "refresh_token" : "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
}

欲しかったaccess tokenとrefresh tokenが取得できました!

APIコールを試す

権限を手に入れたので、後はAPIを好きに叩くことができます。GmailAPI一覧はドキュメントにありますので、たとえばこのコマンドではGmailで作成済みのラベル一覧を出力します。エンドポイントの中にあるuserIdmeに置き換えておくのを忘れないでください。

curl -H "Authorization: Bearer xxxx.xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" \
    https://www.googleapis.com/gmail/v1/users/me/labels

同様に、今年1/1以降で「猫」が含まれるメールを検索し、そのメールIDのリストを取得するのならこうなります。スペースはエスケープしてください。またqの引数にはGmailの検索クエリと全く同等のものが使えます。

curl -H "Authorization: Bearer xxxx.xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" \
    https://www.googleapis.com/gmail/v1/users/me/messages?q="after:2016-01-01%20猫"

access token更新

ここで最も重要な値は上で得たrefresh tokenです。これさえあればいつでも新しいaccess tokenを取得(=更新)できるからです。これはアプリケーション側で保存しておく必要があります。

curl -d client_id=999999999999-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx.apps.googleusercontent.com \
     -d client_secret=xxxxxxxxxxxxxxxxxxxxxxxx \
     -d refresh_token=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx \
     -d grant_type=refresh_token \
     https://accounts.google.com/o/oauth2/token

更新を行ったとしても、更新前に使っていたaccess tokenは有効なまま残ります。つまり、

  • 何か処理を行う前に必ず更新するようにする

    • refresh tokenさえ保存しておけばいい。ここで得たキーは1時間ずっと使える
  • N分ごとに自動更新するようにする(Nは30分、55分など)

    • access tokenをDBか何かに入れておく
    • N分ごとにcronなどでrefresh tokenを使ってaccess tokenを取得し、DBの中のaccess tokenを更新する
    • アプリケーションはDBから取得したaccess tokenで処理をする。このaccess tokenは最短(60-N)分で無効になる

といった選択肢ができることになります。自動処理ができるなら後者の方が簡単そうですね。

AWSの準備

トークンをDynamoDBに入れる

access tokenとrefresh tokenをDynamoDBに入れておきましょう。各種外部サービス*1APIを叩くためのキーを管理するためのテーブルを元から作ってあったので、そこに追加することにしました。このテーブルのプライマリキーはkeyにしています。

f:id:nurenezumi:20160427202441p:plain

更新用Lambdaを用意する

どこかのサーバーからcronでバッチを回してもいいのですが、ここはLambdaで作って自動実行することにします。何をやっているかを明確にするためにライブラリは使っていません。

https://github.com/nurenezumi/ssc-techblog/blob/master/node.js/refresh.jsgithub.com

このコードを実行すると、DynamoDBの指定したテーブル・レコードからrefresh tokenなどの値を取得してaccess tokenを更新し、レコードとタイムスタンプを更新します。DynamoDBへのアクセス権を与えなければならないことには気をつけましょう。

定期的に実行する

作成したLambdaを再度開き、Event sources>Add event sourceと進んで、表示されるダイアログでEvent source typeとしてCloudWatch Events - Scheduleを選択します。

f:id:nurenezumi:20160427205218p:plain

Schedule expressionとして「rate(30 minutes)」を選べば30分ごとに自動実行してくれるようになります。簡単ですね。

トークンを使って何かする

これで常に有効なaccess tokenがDynamoDBに入っている状態となりました。先ほどと同じように、Lambdaから今あるラベルの一覧を取得してログに出力してみます。

var https = require("https");
var querystring = require("querystring");
var AWS = require("aws-sdk");
var dynamo = new AWS.DynamoDB.DocumentClient();
var TABLENAME = "YOUR-DYNAMODB-TABLE-NAME";
var KEYNAME = "YOUR-RECORD-KEY";

var getGoogleApiKey = function(callback) {
    var condition = {
        TableName : TABLENAME,
        KeyConditionExpression : "#k = :val",
        ExpressionAttributeValues : {":val" : KEYNAME},
        ExpressionAttributeNames  : {"#k" : "key"}
    };
    dynamo.query(condition, function(err, data) {
        if (err) { context.fail(err); }
        else { callback(data.Items[0].token); }
    });
};

var kickGet = function(path, token, success, fail) {
    var options = {
        hostname: "www.googleapis.com",
        port: 443,
        path: path,
        method: "GET",
        headers : { "Authorization" : "Bearer " + token }
    };

    var req = https.request(options, function(res) {
        body = "";
        console.log("[DEBUG] response received");
        res.setEncoding("utf8");
        res.on("data", function(chunk) { body += chunk; });
        res.on("end", function() {
            if (res.statusCode == 200) {
                console.log(body);
                var json = JSON.parse(body);
                success(json);
            } else {
                console.error("ERROR!");
                console.error(body);
                console.error(res.errorMessage);
                fail(res);
            }
        });
    });
    
    req.on("error", function(err) { fail(err); });
    req.end();    
};

exports.handler = function(event, context) {
    getGoogleApiKey(function(token) {
        kickGet(
            "https://www.googleapis.com/gmail/v1/users/me/labels",
            token,
            context.succeed,
            context.fail
        );
    });
};
f:id:nurenezumi:20160428164945p:plain

結果はこんな感じです。無事出力に成功しました。

まとめ

OAuthのフロー図を見ると若干面倒くさいように感じられますが、実はこれだけのコードで実現可能なシンプルな処理で構成されていることが分かります。これを使って次はもっと複雑なことにチャレンジしてみたいと思います*2。それでは!

*1:Google、Slack、Salesforceなど

*2:実装よりも記事を書く方が大変だったりしますが

Google Apps APIで新入社員対応(ユーザー追加+組織・グループに追加)

こんにちは井上です。
Google Appsでユーザーを追加する時って、大抵の場合グループにもそのユーザを参加させると思うんです。グループ管理は煩雑になりやすく、役割が重複したものやメンバーの差異がほとんどないものなど、数多くのグループが作られがちです。ですので1ユーザーは複数グループに追加する必要が出てきますが、Google AppsGUIを使ってグループにユーザーを追加しようとすると非常にめんどくさい!

という訳でGoogle Apps APIをたたいてユーザ追加と同時にグループに参加させるようにしてみました。ついでに組織にも所属させるようにしました。普通はGoogle Apps Scriptからやると思いますが、敢えてC#で。

なお、ユーザー削除時はユーザーを削除すればそのユーザーが属しているグループからも勝手に消えるのでGUIで困ってません。ユーザー削除が頻繁にあるなら別ですが、それって会社としてどーなのって感じですし(´・ω・`)

ご丁寧に公式にお手軽スタートマニュアルが用意されています。
.NET quickstart  |  Directory API  |  Google Developers

APIドキュメントはこちら。
Admin SDK: Directory API  |  Google Developers

Google APIs Client Libraryをインストールします。 f:id:ihisa:20160401100908p:plain

簡単なフォームを作ります。(Google Appsのユーザー追加画面風)
f:id:ihisa:20160404134408p:plain

コードは以下の通り。
ちょっと気を付けなければいけないポイントは以下ぐらいです。

  • 組織はUser.OrganizationじゃなくてUser.OrgUnitPathで指定
  • パスワードはポリシーに従った値でないとHTTPステータス400になります。
  • グループ一覧取得時はCustomerプロパティ値を指定しないとHTTPステータス403になります。
using Google.Apis.Admin.Directory.directory_v1;
using Google.Apis.Admin.Directory.directory_v1.Data;
using Google.Apis.Auth.OAuth2;
using Google.Apis.Services;
using Google.Apis.Util.Store;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading;
using System.Windows.Forms;

namespace GoogleAppsManagement
{
    public partial class Main : Form
    {
        UserCredential credential;
        DirectoryService service;
        static string applicationName = "GoogleAppsApplication";
        static string customer = "my_customer"; // この文字列を指定すると実行者のcustomerIdが取得されます。
        static string domain = "@sanwasystem.com";
        IList<OrgUnit> orgunits;
        IList<Group> groups;

        public Main()
        {
            InitializeComponent();
        }

        private void Main_Load(object sender, EventArgs e)
        {
            // UserCredentialの作成
            this.CreateUserCredential();
            
            // DirectoryServiceの作成
            this.CreateDirectoryService();

            // 組織の取得。ここでは組織名称を表示しています。
            orgunits = this.GetOrgunits();
            this.OrgUnitList.DataSource = orgunits.Select(x => x.Name).ToList();

            // グループの取得。ここではグループアドレスを表示しています。
            groups = this.GetGroups();
            this.GroupList.DataSource = groups.Select(x => x.Email).ToList();
        }

        private void AddUser_Click(object sender, EventArgs e)
        {
            string mailAddress = this.mailAddress.Text + domain;

            // ユーザ追加
            User user = new User();
            user.Name = new UserName()
            {
                FamilyName = this.FamilyName.Text,
                GivenName = this.FirstName.Text
            };
            user.PrimaryEmail = mailAddress;
            user.Password = this.Password.Text;
            user.OrgUnitPath = orgunits.Where(x => x.Name == this.OrgUnitList.SelectedValue.ToString()).First().OrgUnitPath; // 選択した組織のOrgUnitPathを指定
            UsersResource.InsertRequest requestUser = service.Users.Insert(user);
            var responseUser = requestUser.Execute();
            
            // グループに追加
            foreach(var row in this.GroupList.SelectedItems)
            {
                MembersResource.InsertRequest requestMember = service.Members.Insert(
                    new Member()
                    {
                        Email = mailAddress,
                        Role = "MEMBER", // 役割。MANAGER / MEMBER / OWNER
                        Type = "USER" // メンバータイプ。CUSTOMER / GROUP / USER
                    },
                    row.ToString()  // グループのIDかメールアドレスを指定なのでメールアドレスを指定しています。
                    );
                var responseGroup = requestMember.Execute();
            }
        }

        private void CreateUserCredential()
        {
            // https://console.developers.google.com/project
            // Google API Consoleからあらかじめ発行したクライアントIDのJSONファイルを指定します。
            // これにより、プロジェクトを実行するとブラウザが立ち上がり認証が行われます。
            using (var stream = new FileStream("client_id.json", FileMode.Open, FileAccess.Read))
            {
                string credPath = System.Environment.GetFolderPath(
                    System.Environment.SpecialFolder.Personal);
                credPath = Path.Combine(credPath, ".credentials/admin-directory_v1-dotnet-quickstart.json");

                credential = GoogleWebAuthorizationBroker.AuthorizeAsync(
                    GoogleClientSecrets.Load(stream).Secrets,
                    new string[]{ 
                        // 実行したい処理に合わせてスコープを指定します。
                        DirectoryService.Scope.AdminDirectoryGroup,
                        DirectoryService.Scope.AdminDirectoryGroupMember,
                        DirectoryService.Scope.AdminDirectoryUser,
                        DirectoryService.Scope.AdminDirectoryCustomer,
                    },
                    "user",
                    CancellationToken.None,
                    new FileDataStore(credPath, true)).Result;
            }
        }

        private void CreateDirectoryService()
        {            
            service = new DirectoryService(new BaseClientService.Initializer()
            {
                HttpClientInitializer = credential,
                ApplicationName = applicationName, // 値は何でも良い
            });
        }
        
        private IList<OrgUnit> GetOrgunits()
        {
            OrgunitsResource.ListRequest orgunitsResourceRequest = service.Orgunits.List(customer);
            return orgunitsResourceRequest.Execute().OrganizationUnits;
        }
        
        private IList<Group> GetGroups()
        {
            GroupsResource.ListRequest groupsResourceRequest = service.Groups.List();
            groupsResourceRequest.Customer = customer;
            return groupsResourceRequest.Execute().GroupsValue;
        }
    }
}

Google AppsGUIは微妙ですがAPIは充実していますので、自分たちで便利にしていけばいいですね!

MySQL Workbenchでデータベースの差分を調べてを更新する方法

こんにちは、ごじぽんです。

今回は MySQL Workbench でデータベースの差分を調べて更新クエリを作成する方法を紹介します。データーベースの差分を埋めたい場合、「CREATE文を出力」→「CREATE文の差分をとる」→「変更クエリの作成」という手順でデータベースを差分を埋めていましたが、もう少し簡単にできる方法を探したところ、MySQL Workbench で簡単に出来たので備忘録込みで書きました。

環境

MySQL Workbench 6.3E(Windows版)

接続設定の作成

前準備として、比較対象の2つのデータベースへの接続設定を作成しておきます。

出力手順

メニューから File -> New Model を選択し、新規モデルの画面を立ち上げます。次に メニューから Model -> Synchronize With Any Source を選択します。 f:id:Derabon:20160330191820p:plain

Introduction

Nextを押して進みましょう。機能説明ですね。2つのデータベースを比較して変更を適用したり、スクリプトの出力ができますという旨のメッセージが書いてあります。

Select Source

比較対象の2つのデータベースの選択画面ですが、Live Database Server を選択し次に進みます。 f:id:Derabon:20160330191905p:plain

Source Database・Target Database

Source Databaseでは 新しいDBの接続 を指定して、Target Database では 古いDBの接続 を指定します。 f:id:Derabon:20160330191933p:plain

Get Source and Target

Nextで進みます。

Select Schemata

対象にするスキーマを選んでOK。 f:id:Derabon:20160330192007p:plain

Fetch Objects

Nextで進みます。

Select Changes to Apply

差分が表示されるので確認できる。見やすくてよい。 f:id:Derabon:20160330192050p:plain

Detected Changes

画面に変更クエリが表示されます。ファイルに保存するボタンやクリップボードにコピーするボタンがあり、画面にもクエリが表示されているので自由に利用できます。 Executeボタン でそのまま実行する事もできます。私の場合はクエリを確認してから、別で実行したいのでクリップボードに保存してキャンセルします。 f:id:Derabon:20160330192111p:plain

Alter Progress

変更クエリが成功したかが分かります。

GUIで分かりやすく差分の確認をしつつ、変更クエリの作成までできるところがとてもよかったです。それと、記事とは関係ありませんが糖質制限により10kg以上減量しました!

.NETでのAPI作成におけるDelegatingHandlerの利用

こんにちは、久しぶりに和朗です。

ASP.NETのControllerの承認フィルターAuthorizeAttributeにはOnAuthorizationメソッドがあり、ここで認証を行うことができますが、 今回は、なんでもできちゃうDelegatingHandlerを利用して認証ロジックの組み込みを行ってみたいと思います。

f:id:teradak:20160328130759p:plain

RequestとResponseの間に入っているので、Controllerに処理を渡さずにResponseすることも可能ですし、Request内容を変更することも可能!

今回利用したもの

  • System.Net.Http.DelegatingHandler
  • System.Security.Principal.GenericIdentity
  • System.Security.Principal.GenericPrincipal
  • System.Web.Http.AuthorizeAttribute

まずは、GenericIdentity

public class CustomeIdentity : System.Security.Principal.GenericIdentity
{
    public CustomeIdentity(string name) : base(name) { }
    /// <summary>
    /// Authorize属性と紐付けるための、プロパティ(ロールA,ロールBなど)
    /// </summary>
    public string[] Roles { set; get; }
}

identityに業務独自のプロパティを設けたい場合に利用。identityは処理のどこからでも参照可能なので便利。

次に、GenericPrincipal

public class CustomePrincipal : System.Security.Principal.GenericPrincipal
{
    public CustomePrincipal(CustomeIdentity identity) : base(identity, identity.Roles) { }
}

ここは、AuthorizeAttributeのroleとの紐付けに利用しました。

そしてメインの、DelegatingHandler

public class CustomeHandler : System.Net.Http.DelegatingHandler
{
    protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
    {
        CustomeIdentity identity = null;
        {   // 認証に利用できるであろう情報
            var headerAuthorization = request.Headers.Authorization.Parameter;
            var fromIp = ((HttpContextWrapper)request.Properties["MS_HttpContext"]).Request.UserHostAddress;
            var targetUrl = request.RequestUri.ToString();
            var method = request.Method.Method;
            /* 
                * 認証ロジック
                * 省略
                */
            identity = new CustomeIdentity("一意な文字列");
        }

        if (identity == null)
        {   // 認証NG
            return Task<HttpResponseMessage>.Factory.StartNew(() =>
                {
                    return new HttpResponseMessage(HttpStatusCode.BadRequest)
                    {
                        Content = new StringContent("Error encountered while attempting to process authorization token")
                    };
                });
        }
        else
        {   // 認証成功
            var principal = new CustomePrincipal(identity);
            this.SetPrincipal(principal);
        }
            
        return base.SendAsync(request, cancellationToken);
    }

    /// <summary>
    /// http://www.asp.net/web-api/overview/security/authentication-and-authorization-in-aspnet-web-api
    /// </summary>
    /// <param name="p"></param>
    private void SetPrincipal(System.Security.Principal.IPrincipal p)
    {
        // 2つのプロパティにセットする必要があります。
        Thread.CurrentPrincipal = p;
        if (HttpContext.Current != null)
        {
            HttpContext.Current.User = p;
        }
    }
}

作成したハンドラをResisterに設定

public static class WebApiConfig
{
    public static void Register(HttpConfiguration config)
    {
        // カスタムハンドラの追加
        config.MessageHandlers.Add(new CustomeHandler());
    }
}

最後に、ApiControllerに利用可能なロールを設定

[System.Web.Http.Authorize(Roles = "ロールA,ロールB")]
public class CustomeController : System.Web.Http.ApiController
{
}

GenericPrincipalのコンストラクタで渡しているロールと紐付けられています。 コントローラやメソッドの属性としてAuthorizeが設定可能です。 コントローラ、メソッド単位でロールを意識することになり、安心感があります。

まとめ

今回の内容により、接続元(接続ユーザ)ごとに利用可能なURLをプログラムレベルで定義することが可能となり コントローラ作成時に常にセキュリティを意識した実装を行うことになります。 もう一つのメリットとして、ログ出力とidentityの連動もメリットだと考えます。 identityを利用することによって接続元の情報にどこからでもアクセス可能となるからです。

AWSの社内勉強会を開きました

こんにちは、AWS担当のwakです。

AWSを弊社で利用し始めて2年ほど経ちますが、未だにEC2やRDSのみしか念頭に置いていないメンバーがいるのも事実です。「AWSにはこんな機能もあるよ!」「これを使うとシステム構築が楽になるかも!」「できることに幅が広がるかも!」という知識の底上げを図るために社内勉強会を実施しました。

EC2 Run Commandは東京リージョンで使えるようになったばかりですが、メンバーからは「こういうことがやりたかった!」と喝采が上がりました。SNS/SQSはうまく活用できればシステム構成を大幅にシンプルかつ強固にできる可能性がありますし、Lambda+API Gatewayは特定の状況ではサーバーを排除することすらできる極めて強力なツールです。今すぐに取り込むことはかなわなくとも、存在を知らなければ採用する・しないという選択肢自体を持ち得ません。

インターネットは「学習の高速道路」だという話がありますが、それならばAWSは高速道路どころかワープポイントのようなものです。当たり前のように乗りこなしていきましょう。

ASP.NET Web APIでログを綺麗に出す

井上です。

Web Request/Responseのログをお手軽に出しましょうというお話です。
ログ出力のサンプルは多々あるのですが、パラメータを全部出していたりとかはあんまりないというところから。

環境

Visual Studio Pro 2013
.NET Framework 4.5.3
ASP.NET Web API

Json.NETをGetする

f:id:ihisa:20160120171820p:plain

ログ出力クラスを作る

https://msdn.microsoft.com/ja-jp/library/system.web.http.filters.actionfilterattribute(v=vs.118).aspxクラスを継承してOnActionExecutedをオーバーライドします。
最初コントローラーに引き渡されるパラメータをリフレクションしまくってクラスプロパティ値出してましたが、JsonConvertでJson形式で出力する良い感じです。

using Newtonsoft.Json;
using System.Web.Http.Filters;

public class LoggingAttribute : ActionFilterAttribute
{  
    public override void OnActionExecuting(System.Web.Http.Controllers.HttpActionContext actionContext)
    {
        var log = new
        {
            LogType = "OnActionExecuting",
            Controller = actionContext.ControllerContext.Controller.ToString(),
            Method = actionContext.Request.Method.Method,
            RequestUri = actionContext.Request.RequestUri,
            RequestHeaders = actionContext.Request.Headers,
        };
        Logger.WriteLog(Logger.LogLevel.Info, JsonConvert.SerializeObject(log));
        base.OnActionExecuting(actionContext);
    }

    public override void OnActionExecuted(HttpActionExecutedContext actionExecutedContext)
    {
        // 例外発生時は例外フィルターより先に呼び出されるのでResponse値をチェックしたうえで出力する
        var log = new
        {
            LogType = "OnActionExecuted",
            HTTPStatus = (actionExecutedContext.Response == null) ? "" : actionExecutedContext.Response.StatusCode.ToString(),
            Request = actionExecutedContext.Request.ToString(),
            Parameter = actionExecutedContext.ActionContext.ActionArguments,
            Response = (actionExecutedContext.Response == null || actionExecutedContext.Response.Content == null) ? "" : actionExecutedContext.Response.Content.ReadAsStringAsync().Result,
        };
        Logger.WriteLog(Logger.LogLevel.Info, JsonConvert.SerializeObject(log));
        base.OnActionExecuted(actionExecutedContext);
    }
}

System.Web.Http.Filters.ActionFilterAttributeにはOnActionExecutingOnActionExecutedがありますが、HttpActionExecutedContextにRequestの内容も含まれていますので、OnActionExecutedだけ実装しておけばログ内容としては足りるかと思います。
尚、コード中のLoggerはlog4netを用いたログ出力クラスです。
例フィルターを作る場合はhttps://msdn.microsoft.com/ja-jp/library/system.web.http.filters.exceptionfilterattribute(v=vs.118).aspxクラスを継承してOnExceptionをオーバーライドします。

コントローラーの基底クラスを定義し、フィルターを付ける

基底クラスに以下の通りフィルターを定義しておきます。

[Logging]
public class BaseApiController : ApiController

コントローラーで基底クラスを定義する

    [RoutePrefix("hogehoge")]
    public class HogeHogeController : BaseApiController
    {
        [HttpGet]
        public IHttpActionResult HogeHoGet(string param1, string param2, int param3)
        {
            var result = new
            {
                param1 = "param1は" + param1,
                param2 = "param2は" + param2,
                param3 = "param3は" + param3
            };
            return Ok(result);
        }
    }

ログの確認

http://localhost:61369/hogehoge?param1=sanwa&param2=system&param3=123で呼び出しログを確認します。

OnActionExecutingのログ

{"2016-02-24 13:21:14,195":{"level":"INFO ","message":{"LogType":"OnActionExecuting","Controller":"Ssc.Ttb.Api.Front.Controllers.v1.HogeHogeController","Method":"GET","RequestUri":"http://localhost:61369/hogehoge?param1=sanwa&param2=system&param3=123","RequestHeaders":[{"Key":"Connection","Value":["Keep-Alive"]},{"Key":"Accept","Value":["text/html","application/xhtml+xml","*/*"]},{"Key":"Accept-Encoding","Value":["gzip","deflate"]},{"Key":"Accept-Language","Value":["ja-JP"]},{"Key":"Host","Value":["localhost:61369"]},{"Key":"User-Agent","Value":["Mozilla/5.0","(Windows NT 6.1; WOW64; Trident/7.0; rv:11.0)","like","Gecko"]}]}}}

JSON Viewerで見るとこんな感じです。 f:id:ihisa:20160224174912p:plain

OnActionExecutedのログ

{"2016-02-24 13:21:14,195":{"level":"INFO ","message":{"LogType":"OnActionExecuted","HTTPStatus":"OK","Request":"Method: GET, RequestUri: 'http://localhost:61369/hogehoge?param1=sanwa&param2=system&param3=123', Version: 1.1, Content: System.Web.Http.WebHost.HttpControllerHandler+LazyStreamContent, Headers:\r\n{\r\n Connection: Keep-Alive\r\n Accept: text/html\r\n Accept: application/xhtml+xml\r\n Accept: */*\r\n Accept-Encoding: gzip\r\n Accept-Encoding: deflate\r\n Accept-Language: ja-JP\r\n Host: localhost:61369\r\n User-Agent: Mozilla/5.0\r\n User-Agent: (Windows NT 6.1; WOW64; Trident/7.0; rv:11.0)\r\n User-Agent: like\r\n User-Agent: Gecko\r\n}","Parameter":{"param1":"sanwa","param2":"system","param3":123},"Response":"{\"param1\":\"param1はsanwa\",\"param2\":\"param2はsystem\",\"param3\":\"param3は123\"}"}}}

同じくJSON Viewerで見ます。 f:id:ihisa:20160224175027p:plain

綺麗に出ました。