Azureの小ネタ (改)

~Azureネタを中心に、色々とその他の技術的なことなどを~

Azure Web Apps の Easy Auth 情報をASP.NET Coreで受け取る

Azure App Service の Web AppsにはAzure ADで簡単に認証を設定できたりします(Easy Authというらしい)。

Azure App Service での認証と承認 | Microsoft Docs

これを使うとアプリ側に認証コード書かなくて済むのと、ASP.NET (Coreじゃない) の場合は、

すべての言語フレームワークで、App Service はユーザーの要求を要求ヘッダーに挿入することによってコードで要求を使用できるようにします。 ASP.NET 4.6 アプリの場合、App Service は認証されたユーザーの要求で ClaimsPrincipal.Current を設定するので、標準の .NET コード パターン ([Authorize] 属性など) に従うことができます。

フレームワーク側でPrincipalに適宜情報を設定してくれたりしますが、ASP.NET Core 2.xではサポートされていません。どうすればいいのかググってたらいくつか方法があったので、備忘録をブログっておきます。

stackoverflow の以下の記事。

stackoverflow.com

抜粋して拡張メソッド化すると以下のような感じです。app.UseEasyAuth() とかすればOKです。

パイプライン途中でヘッダ情報からGenericPrincipalを生成して、Context.Userに設定する方法です。

   public static class ApplicationBuilderExtension
    {
        public static void UseEasyAuth(this IApplicationBuilder app)
        {
            app.Use(async (context, next) =>
            {
                // Create a user on current thread from provided header
                if (context.Request.Headers.ContainsKey("X-MS-CLIENT-PRINCIPAL-ID"))
                {
                    // Read headers from Azure
                    var azureAppServicePrincipalIdHeader = context.Request.Headers["X-MS-CLIENT-PRINCIPAL-ID"][0];
                    var azureAppServicePrincipalNameHeader = context.Request.Headers["X-MS-CLIENT-PRINCIPAL-NAME"][0];

                    // Create claims id
                    var claims = new Claim[]
                    {
                        new System.Security.Claims.Claim("http://schemas.microsoft.com/identity/claims/objectidentifier", azureAppServicePrincipalIdHeader),
                        new System.Security.Claims.Claim("name", azureAppServicePrincipalNameHeader)
                    };

                    // Set user in current context as claims principal
                    var identity = new GenericIdentity(azureAppServicePrincipalNameHeader);
                    identity.AddClaims(claims);

                    // Set current thread user to identity
                    context.User = new GenericPrincipal(identity, null);
                }
                await next.Invoke();
            });
        }
    }

もう1つは、こちらの記事です。

stackoverflow.com

こちらも抜粋すると以下の通り。上との違いは、/.auth/me にリクエストなげて自身の情報をゲットしてクレーム作っているとこでしょうか。

アプリケーションでは、/.auth/me を呼び出して認証されたユーザーの追加の詳細を取得することもできます。

 public static void UseEasyAuth2(this IApplicationBuilder app)
        {
            app.Use(async (context, next) =>
            {
                // Create a user on current thread from provided header
                if (context.Request.Headers.ContainsKey("X-MS-CLIENT-PRINCIPAL-ID"))
                {
                    // Read headers from Azure
                    var azureAppServicePrincipalIdHeader = context.Request.Headers["X-MS-CLIENT-PRINCIPAL-ID"][0];
                    var azureAppServicePrincipalNameHeader = context.Request.Headers["X-MS-CLIENT-PRINCIPAL-NAME"][0];

                    #region extract claims via call /.auth/me

                    //invoke /.auth/me
                    var cookieContainer = new CookieContainer();
                    HttpClientHandler handler = new HttpClientHandler()
                    {
                        CookieContainer = cookieContainer
                    };
                    string uriString = $"{context.Request.Scheme}://{context.Request.Host}";
                    foreach (var c in context.Request.Cookies)
                    {
                        cookieContainer.Add(new Uri(uriString), new Cookie(c.Key, c.Value));
                    }

                    string jsonResult;
                    using (HttpClient client = new HttpClient(handler))
                    {
                        var res = await client.GetAsync($"{uriString}/.auth/me");
                        jsonResult = await res.Content.ReadAsStringAsync();
                    }

                    //parse json
                    var obj = JArray.Parse(jsonResult);

                    string user_id = obj[0]["user_id"].Value<string>(); //user_id

                    // Create claims id
                    List<Claim> claims = new List<Claim>();
                    foreach (var claim in obj[0]["user_claims"])
                    {
                        claims.Add(new Claim(claim["typ"].ToString(), claim["val"].ToString()));
                    }

                    // Set user in current context as claims principal
                    var identity = new GenericIdentity(azureAppServicePrincipalIdHeader);
                    identity.AddClaims(claims);

                    #endregion

                    // Set current thread user to identity
                    context.User = new GenericPrincipal(identity, null);
                }

                await next.Invoke();
            });

リクエストごとに呼ばれるよりはキャッシュしたりしたほうが実用的でしょう。どちらの方法を使うかはケースバイケースで。

Power BI で ストリーミングデータ

以下のDocsを試したので備忘録

Power BI のリアルタイム ストリーミング - Power BI | Microsoft Docs

データセット作成

リアルタイムデータセットを作成しておき、データソースを選択する。APIを選択するとPBI側にデータをPOSTできるAPIが作られます。

f:id:StateMachine:20180625155239p:plain

データセットのスキーマを定義します。よくある温度と湿度を追加しておきます。ストリーミングデータなので時間が必要です。

f:id:StateMachine:20180625155809p:plain

POST先のAPIが表示されます。

f:id:StateMachine:20180625155920p:plain

ダッシュボードの作成

リアルタイムデータのタイルを追加します。

f:id:StateMachine:20180625160129p:plain

データセットを選択し、表示するグラフの詳細を定義します。以下の場合ですと、温度を5分枠で折れ線グラフ表示することになります。

f:id:StateMachine:20180625160254p:plain

さらに湿度グラフと、テキストを表示するタイルを追加して、全体的に以下のようなレイアウトを実装します。

f:id:StateMachine:20180625160509p:plain

それっぽいデータの用意

データの用意が面倒ですので、以下より適当に温度と湿度を含むデータ(CSV)をダウンロードしておきます。

気象庁|過去の気象データ・ダウンロード

リアルタイムデータですので、このデータの時間軸は無視しておきます。

POSTするプログラム

適当にCSVからデータを読み込んでJSONでPOSTするプログラムを作成します。 先ほども述べたとおりPOSTするときは現在時刻にしておきます、1秒間隔でPOSTしていきます。 POST時の制限は、DocsのURLからたどれます。

ちょと動かすだけなら、適当にランダムな値をPOSTするだけでも問題ないと思いますけど、なんとなく意味ある値にしたかったので気象データ使いました。

  class Program
    {
        string uri = "https://api.powerbi.com/beta/.....";

        static void Main(string[] args)
        {
            new Program().Run().GetAwaiter().GetResult();
        }

        public async Task Run()
        {
            using (var reader = File.OpenText("data.csv"))
            {
                // Skip headers
                for (int i = 0; i < 6; i++) reader.ReadLine();
                var csv = new CsvReader(reader);
                while (await csv.ReadAsync())
                {
                    DateTime now = DateTime.Parse(csv[0]);
                    double tempu = double.Parse(csv[1]);
                    double humid = double.Parse(csv[9]);

                    var ds = new StreamingDataset()
                    {
                        Datetime = DateTime.Now,
                        Temperature = tempu,
                        Humidity = humid
                    };

                    await Post(ds);
                    await Task.Delay(TimeSpan.FromMilliseconds(1000));
                    Console.WriteLine($"{now} : {tempu} {humid}");
                }
            }
            Console.ReadLine();
        }

        private async Task Post(StreamingDataset ds)
        {
            var client = new HttpClient();
            var content = JsonConvert.SerializeObject(ds);
            await client.PostAsync(uri, new StringContent(content));
        }
    }
    public class StreamingDataset
    {
        [JsonProperty("datetime")]
        public DateTime Datetime { get; set; }

        [JsonProperty("temperature")]
        public double Temperature { get; set; }

        [JsonProperty("humidity")]
        public double Humidity { get; set; }
    }

実行結果

プログラムを実行するとPBIのDashboardがニョロニョロと動き始めます。

f:id:StateMachine:20180625161351p:plain

とりあえず、データソースが直接データをPOSTできるなら、これもありでしょうか。

以上。

WebJobs/Functions のCron式

WebJobs/FunctionsのCron式の備忘録

以下にCron式について触れられているが申し訳程度。

docs.microsoft.com

以下のCheat Sheetが参考になる。

Azure Functions - Time Trigger (CRON) Cheat Sheet | codehollow

元々は、第N曜日的な実行をしたくて、UNIX系のCronだと書けないぽいし、元のDocsはしょぼいしで、どうしたものかと調べていたら、Azureでは問題ないらしい。

いか第3水曜の11時(UTC 2時)で試したところ一応動いた。

0 0 2 15-21 * WED

以上