diff --git a/README.md b/README.md index c3a871a..b2135b1 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,13 @@ -looker_embed_sso_examples -=================== +## Looker Embed SSO Examples Example code to use the Ruby Embed SSO API in various languages +- [C#](/csharp_example.cs) +- [Elixir](/elixir) +- [Java](/LookerEmbedClientExample.java) +- [PHP](/sso_embed.php) +- [NodeJS](/node_example.js) +- [Ruby](/ruby_example.rb) + ### [Link to offical Looker Single Sign-on Embedding documentation](https://docs.looker.com/reference/embedding/sso-embed) ### [Link to SSO URL build testing tool](https://fabio-looker.github.io/looker_sso_tool) - diff --git a/elixir/.gitignore b/elixir/.gitignore new file mode 100644 index 0000000..0304f65 --- /dev/null +++ b/elixir/.gitignore @@ -0,0 +1,2 @@ +_build +deps diff --git a/elixir/README.md b/elixir/README.md new file mode 100644 index 0000000..7367786 --- /dev/null +++ b/elixir/README.md @@ -0,0 +1,23 @@ +### Usage + +``` +$ cd elixir +$ mix deps.get +$ mix deps.compile +$ mix run elixir_example.exs +``` + +### Options + +|*Key* | *Required* | *Default* | *Description* | +|:--------------------|:-----------|:---------------|-------------------------------------------------------------------------------------------| +| `embed_url` | Yes | NA | Looker relative embed url | +| `session_length` | No | 1800 (30 mins) | The login session lenght (validiy of the SSO URL). Default: 15 mins | +| `host` | Yes | NA | Looker host | +| `secret` | Yes | NA | Looker API secret | +| `user` | Yes | NA | A Map of user data (id, first_name, last_name). This will be used to create embed user | +| `permissions` | No | NA | A list of looker permissions the embed user should have | +| `models` | No | NA | A list of looker models that should be accessible by the embed user | +| `group_ids` | No | NA | A list of looker group ids that the embed user should be added | +| `external_group_id` | No | NA | External group id for the embed user | +| `user_attributes` | No | NA | A Map of user filters/attributes that are applicable for the embed user | diff --git a/elixir/elixir_example.exs b/elixir/elixir_example.exs new file mode 100644 index 0000000..da12f89 --- /dev/null +++ b/elixir/elixir_example.exs @@ -0,0 +1,133 @@ +defmodule LookerEmbed do + @moduledoc """ + Module for generating Looker embed SSO URL + """ + + @doc """ + opts[:embed_url] Looker relative embed url + opts[:session_length] The login session lenght (validiy of the SSO URL). Default: 30 mins + opts[:host] Looker host + opts[:secret] Looker API embed secret + opts[:user] A Map of user data (id, first_name, last_name). This will be used to create embed user + opts[:permissions] A list of looker permissions the embed user should have + opts[:models] A list of looker models that should be accessible by the embed user + opts[:group_ids] A list of looker group ids that the embed user should be added + opts[:external_group_id] External group id for the embed user + opts[:user_attributes] A Map of user filters/attributes that are applicable for the embed user + + """ + def generate_sso_url(%{user: user} = opts) do + session_length = opts[:session_length] || (30 * 60) + host = opts.host + + embed_path = "/login/embed/#{URI.encode_www_form(opts.embed_url)}" + + url_options = %{ + host: host, + secret: opts.secret, + external_user_id: user.id |> wrap_quotes, + first_name: user.first_name |> wrap_quotes, + last_name: user.last_name |> wrap_quotes, + permissions: opts.permissions, + models: opts.models, + group_ids: opts.group_ids, + external_group_id: opts.external_group_id |> wrap_quotes, + user_attributes: opts.user_attributes, + access_filters: %{}, # we pass empty map because looker requires this parameter + session_length: session_length |> to_string, + embed_path: embed_path, + nonce: SecureRandom.urlsafe_base64(16) |> wrap_quotes, + time: DateTime.utc_now |> DateTime.to_unix |> to_string + } + + query_string = get_query_string(url_options) + + "https://#{host}#{embed_path}?#{query_string}" + end + + # private + + defp get_signature(opts) do + string_data = "#{opts[:host]}\n" + string_data = string_data <> opts[:embed_path] <> "\n" + string_data = string_data <> opts[:nonce] <> "\n" + string_data = string_data <> opts[:time] <> "\n" + string_data = string_data <> opts[:session_length] <> "\n" + string_data = string_data <> opts[:external_user_id] <> "\n" + string_data = string_data <> encode_json(opts[:permissions]) <> "\n" + string_data = string_data <> encode_json(opts[:models]) <> "\n" + + # attributes supported in new looker api version + string_data = if is_nil(opts[:group_ids]) do + string_data + else + string_data <> encode_json(opts[:group_ids]) <> "\n" + end + + string_data = if is_nil(opts[:external_group_id]) do + string_data + else + string_data <> opts[:external_group_id] <> "\n" + end + + string_data = string_data <> encode_json(opts[:user_attributes]) <> "\n" + string_data = string_data <> encode_json(opts[:access_filters]) + + :crypto.hmac(:sha, opts[:secret], string_data) + |> Base.encode64 + end + + defp get_query_string(opts) do + params = %{ + nonce: opts.nonce, + time: opts.time, + session_length: opts.session_length, + external_user_id: opts.external_user_id, + permissions: encode_json(opts.permissions), + models: encode_json(opts.models), + access_filters: encode_json(opts.access_filters), + first_name: opts.first_name, + last_name: opts.last_name, + signature: get_signature(opts), + group_ids: encode_json(opts.group_ids), + external_group_id: opts.external_group_id, + user_attributes: encode_json(opts.user_attributes), + force_logout_login: true + } + + params |> URI.encode_query + # Note: URI.encode query does not wrap values of query string in a quote. + # this creates issues as looker expects string values to be inside quote. + # For example, a map %{name: "Test"} will be encoded to [name=Taher] + # but looker wants it to be, [name="Taher"] + # The wrapping on quotes is already done at source in `generate_sso_url` + end + + defp encode_json(value) when not is_nil(value) do + Poison.encode!(value) + end + + defp encode_json(_), do: "" + + # This is function wraps values inside double quotes. + # This is required for query string for looker as its very strict in format + defp wrap_quotes(value), do: "\"#{value}\"" +end + +options = %{ + embed_url: "/embed/looker_report", + host: "app.looker.com", + secret: "mysekret", + user: %{ + id: 100, + first_name: "Taher", + last_name: "Dhilawala" + }, + permissions: ~w(access_data see_looks)s, + models: "external", + group_ids: [1], + external_group_id: 2, + user_attributes: [] +} + +IO.puts LookerEmbed.generate_sso_url(options) diff --git a/elixir/mix.exs b/elixir/mix.exs new file mode 100644 index 0000000..e9ff3ed --- /dev/null +++ b/elixir/mix.exs @@ -0,0 +1,31 @@ +defmodule LookerEmbed.Mixfile do + use Mix.Project + + def project do + [ + app: :looker_embed, + version: "0.0.1", + elixir: "~> 1.5", + deps: deps() + ] + end + + + # Configuration for the OTP application. + # + # Type `mix help compile.app` for more information. + def application do + [] + end + + # Specifies your project dependencies. + # + # Type `mix help deps` for examples and options. + defp deps do + [ + {:poison, "~> 2.2"}, + {:secure_random, "~> 0.5"}, + {:timex, "~> 3.1"} + ] + end + end diff --git a/elixir/mix.lock b/elixir/mix.lock new file mode 100644 index 0000000..f7047ad --- /dev/null +++ b/elixir/mix.lock @@ -0,0 +1,14 @@ +%{"certifi": {:hex, :certifi, "1.2.1", "c3904f192bd5284e5b13f20db3ceac9626e14eeacfbb492e19583cf0e37b22be", [], [], "hexpm"}, + "combine": {:hex, :combine, "0.10.0", "eff8224eeb56498a2af13011d142c5e7997a80c8f5b97c499f84c841032e429f", [], [], "hexpm"}, + "gettext": {:hex, :gettext, "0.13.1", "5e0daf4e7636d771c4c71ad5f3f53ba09a9ae5c250e1ab9c42ba9edccc476263", [], [], "hexpm"}, + "hackney": {:hex, :hackney, "1.8.6", "21a725db3569b3fb11a6af17d5c5f654052ce9624219f1317e8639183de4a423", [], [{:certifi, "1.2.1", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "5.0.2", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "1.0.1", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "1.0.2", [hex: :mimerl, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "1.1.1", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}], "hexpm"}, + "httpoison": {:hex, :httpoison, "0.11.2", "9e59f17a473ef6948f63c51db07320477bad8ba88cf1df60a3eee01150306665", [], [{:hackney, "~> 1.8.0", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm"}, + "idna": {:hex, :idna, "5.0.2", "ac203208ada855d95dc591a764b6e87259cb0e2a364218f215ad662daa8cd6b4", [], [{:unicode_util_compat, "0.2.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm"}, + "metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [], [], "hexpm"}, + "mimerl": {:hex, :mimerl, "1.0.2", "993f9b0e084083405ed8252b99460c4f0563e41729ab42d9074fd5e52439be88", [], [], "hexpm"}, + "poison": {:hex, :poison, "2.2.0", "4763b69a8a77bd77d26f477d196428b741261a761257ff1cf92753a0d4d24a63", [], [], "hexpm"}, + "secure_random": {:hex, :secure_random, "0.5.1", "c5532b37c89d175c328f5196a0c2a5680b15ebce3e654da37129a9fe40ebf51b", [], [], "hexpm"}, + "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.1", "28a4d65b7f59893bc2c7de786dec1e1555bd742d336043fe644ae956c3497fbe", [], [], "hexpm"}, + "timex": {:hex, :timex, "3.1.24", "d198ae9783ac807721cca0c5535384ebdf99da4976be8cefb9665a9262a1e9e3", [], [{:combine, "~> 0.7", [hex: :combine, repo: "hexpm", optional: false]}, {:gettext, "~> 0.10", [hex: :gettext, repo: "hexpm", optional: false]}, {:tzdata, "~> 0.1.8 or ~> 0.5", [hex: :tzdata, repo: "hexpm", optional: false]}], "hexpm"}, + "tzdata": {:hex, :tzdata, "0.5.12", "1c17b68692c6ba5b6ab15db3d64cc8baa0f182043d5ae9d4b6d35d70af76f67b", [], [{:hackney, "~> 1.0", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm"}, + "unicode_util_compat": {:hex, :unicode_util_compat, "0.2.0", "dbbccf6781821b1c0701845eaf966c9b6d83d7c3bfc65ca2b78b88b8678bfa35", [], [], "hexpm"}}