Lookerダッシュボード 外部サイト埋め込み（一時URL発行）

# はじめに
Lookerのダッシュボードを外部サイトのログインユーザに対して公開したい場合に利用できる
SSO埋め込み処理機能を実装する機会があったので
備忘録的に記載させていただきます。

※ Pythonのパターンを例に記載させていただきます。
※ 他言語のパターンはLooker様が公開している Githubのサンプルをご確認ください。

https://github.com/looker/looker_embed_sso_examples

# Lookerのダッシュボード 埋め込みパターン

## パブリック埋め込み
- ダッシュボードの公開設定（Public Access）をONにした状態
- ログイン無しで誰でも閲覧可能な状態
- `iframe` を利用し、外部埋め込み可能
- 誰でも閲覧できてしまう為、利用場面は限られる

## プライベート埋め込み
- ダッシュボードの公開設定（Public Access）をOFFにした状態
- Lookerへログインしており、権限を持つユーザだけが閲覧可能な状態
- Lookerへログイン済みのユーザしか見ないのであれば `iframe` を利用し、外部埋め込み可能
- Lookerへログインしていない場合、`iframe` 部分にLookerログイン画面が表示される

## SSOの埋め込み（本記事で記載）
- 埋め込みたい外部サイトでログイン認証を済ませた上で、埋め込みユーザと一時URLを発行
- `iframe` を利用し、外部埋め込み可能
- 外部サイトでの認証のみで、Lookerログインは不要

https://cloud.google.com/looker/docs/single-sign-on-embedding?hl=ja

# SSO埋め込み処理を行う場合のざっくりとした流れ
1. 外部サイトによるログイン認証
2. ダッシュボードの一時URL発行
3. `iframe`によるダッシュボード 表示

# 一時URL発行方法
## 1. Looker 埋め込みURL発行　暗号鍵取得

1. 管理画面アクセス権限を持ったユーザでLookerへログイン
2. [管理]→[プラットフォーム]→[埋め込み]ページへ移動
3. `Embed SSO Authentication` の項目を `Enabled` へ変更し `Update` を選択
4. `Embed Secret`の項目で `Reset Secret`を選択（暗号鍵が発行される）

※ 暗号鍵が外部へ漏れた場合、全てのダッシュボードへアクセスが可能となるので、厳重に保管してください

![LookerEmbedSecret取得.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2704263/8052e9f6-25b0-9782-0af0-55f214be2911.png)

## 2. 一時URL発行

- ローカルで試す場合は `test()` の各パラメータを埋めた状態で処理を実行することで一時URLを発行可能
- 実際に利用する場合は一時URL発行API等を作成しておき、ログイン認証後
  一時URLを発行し`iframe`へ埋め込むなどの対応が必要

python_example.py
```
import urllib
import base64
import json
import time
import binascii
import os
from hashlib import sha1
import six
import six.moves.urllib as urllib
import hmac



def to_ascii(s):
  """Compatibility function for converting between Python 2.7 and 3 calls"""
  if isinstance(s, six.text_type):
    return s
  elif isinstance(s, six.binary_type):
    return "".join(map(chr, map(ord, s.decode(encoding='UTF-8'))))
  return s

class Looker:
  def __init__(self, host, secret):
    self.secret = secret
    self.host = host


class User:
  def __init__(self, id=id, first_name=None, last_name=None,
               permissions=[], models=[], group_ids=[], external_group_id=None,
               user_attributes={}, access_filters={}):
    self.external_user_id = json.dumps(id)
    self.first_name = json.dumps(first_name)
    self.last_name = json.dumps(last_name)
    self.permissions = json.dumps(permissions)
    self.models = json.dumps(models)
    self.access_filters = json.dumps(access_filters)
    self.user_attributes = json.dumps(user_attributes)
    self.group_ids = json.dumps(group_ids)
    self.external_group_id = json.dumps(external_group_id)


class URL:
  def __init__(self, looker, user, session_length, embed_url, force_logout_login=False):
    self.looker = looker
    self.user = user
    self.path = '/login/embed/' + urllib.parse.quote_plus(embed_url)
    self.session_length = json.dumps(session_length)
    self.force_logout_login = json.dumps(force_logout_login)

  def set_time(self):
    self.time = json.dumps(int(time.time()))

  def set_nonce(self):
    self.nonce = json.dumps(to_ascii(binascii.hexlify(os.urandom(16))))

  def sign(self):
    #  Do not change the order of these
    string_to_sign = "\n".join([self.looker.host,
                                self.path,
                                self.nonce,
                                self.time,
                                self.session_length,
                                self.user.external_user_id,
                                self.user.permissions,
                                self.user.models,
                                self.user.group_ids,
                                self.user.external_group_id,
                                self.user.user_attributes,
                                self.user.access_filters])

    signer = hmac.new(bytearray(self.looker.secret, 'UTF-8'), string_to_sign.encode('UTF-8'), sha1)
    self.signature = base64.b64encode(signer.digest())

  def to_string(self):
    self.set_time()
    self.set_nonce()
    self.sign()

    params = {'nonce':               self.nonce,
              'time':                self.time,
              'session_length':      self.session_length,
              'external_user_id':    self.user.external_user_id,
              'permissions':         self.user.permissions,
              'models':              self.user.models,
              'group_ids':           self.user.group_ids,
              'external_group_id':   self.user.external_group_id,
              'user_attributes':     self.user.user_attributes,
              'access_filters':      self.user.access_filters,
              'signature':           self.signature,
              'first_name':          self.user.first_name,
              'last_name':           self.user.last_name,
              'force_logout_login':  self.force_logout_login}

    query_string = '&'.join(["%s=%s" % (key, urllib.parse.quote_plus(val)) for key, val in params.items()])

    return "%s%s?%s" % (self.looker.host, self.path, query_string)


def test():
  looker = Looker('https://xxxxxxxx.looker.com', '3109470179079012jfiaowfioa0129310fwaejpawfaw....') # テナントURL, 暗号鍵

  user = User(57, # ダッシュボードID
              first_name='Embed Wil', # 埋め込みユーザ 名
              last_name='Krouse', # 埋め込みユーザ 姓
              permissions=['see_lookml_dashboards', 'access_data'], # 埋め込みユーザ 閲覧権限
              models=['thelook'], # 埋め込みユーザ 閲覧可能モデル
              group_ids=[5,4], # 埋め込みユーザ 所属グループ
              external_group_id='awesome_engineers' # 埋め込みユーザ ID
              ) 

  fifteen_minutes = 15 * 60 # 有効期間

  url = URL(looker, user, fifteen_minutes, "/embed/dashboards/3", force_logout_login=True)

  print("https://" + url.to_string())

test()
```

https://github.com/looker/looker_embed_sso_examples/blob/master/python_example.py


### テナントURL
- 契約しているLookerテナントのベースURL
- 例: https://xxxxxxxx.looker.com

### 暗号鍵
- Looker 埋め込みURL発行　暗号鍵取得 で取得した暗号鍵
- 例: 3109470179079012jfiaowfioa0129310fwaejpawfaw

### ダッシュボードID
- 一時URLで表示したいダッシュボードID
- Lookerの対象ダッシュボードURLから確認可能（dashboards/{id}）
- 例: 57

### 埋め込みユーザ 名
- SSO方式の場合は外部サイトでログインしているユーザの名 等
- システム・運用的にわかりやすい命名でも問題なし

### 埋め込みユーザ 姓
- SSO方式の場合は外部サイトでログインしているユーザの姓 等
- システム・運用的にわかりやすい命名でも問題なし

### 埋め込みユーザ 閲覧権限
- 一時URL接続に必要な埋め込みユーザの閲覧権限
- `access_data`を付与することで、最低限アクセス可能
- その他権限に関しては下記参照

https://cloud.google.com/looker/docs/single-sign-on-embedding

### 埋め込みユーザ 閲覧可能モデル
- 一時URL接続に必要な埋め込みユーザの閲覧可能モデル
- 表示対象のダッシュボードに利用しているモデルを含めておく

### 埋め込みユーザの所属グループ
- 一時URL接続に必要な埋め込みユーザの所属グループ
- `Embed Users` など、わかりやすいグループを作成しておくと管理しやすい

### 埋め込みユーザ ID
- ユーザを一意に識別するID
- 既に存在するIDを指定した場合は各種情報が上書きされる
- 外部サイトでログインしたユーザ毎に権限を変える場合は、ユーザ一意のIDを指定

### 有効期間
- 一時URLが発行されてから無効になるまでの有効期間
- 有効期間の有無にかかわらず、一度アクセスしたURLは使用不可となる


# おわりに
殆ど公開されている公式ドキュメントに記載されている内容ですが
情報が多く、記載箇所がわかりずらかったので、実装に必要な最低限の情報だけ記載しています。
詳しくは公式ドキュメントをご確認ください。