Giter Site home page Giter Site logo

tompave / fun_with_flags_ui Goto Github PK

View Code? Open in Web Editor NEW
125.0 4.0 27.0 3.05 MB

Web dashboard for the FunWithFlags Elixir package

License: MIT License

Elixir 74.61% HTML 24.50% CSS 0.57% JavaScript 0.32%
elixir plug web-dashboard feature-flags feature-toggles phoenix-framework

fun_with_flags_ui's Introduction

FunWithFlags.UI

Mix Tests Code Quality
Hex.pm

A Web dashboard for the FunWithFlags Elixir package.

How to run

FunWithFlags.UI is just a plug and it can be run in a number of ways. It's primarily meant to be embedded in a host Plug application, either Phoenix or another Plug app.

Mounted in Phoenix

The router plug can be mounted inside the Phoenix router with Phoenix.Router.forward/4.

defmodule MyPhoenixAppWeb.Router do
  use MyPhoenixAppWeb, :router

  pipeline :mounted_apps do
    plug :accepts, ["html"]
    plug :put_secure_browser_headers
  end

  scope path: "/feature-flags" do
    pipe_through :mounted_apps
    forward "/", FunWithFlags.UI.Router, namespace: "feature-flags"
  end
end

Note: There is no need to add :protect_from_forgery to the :mounted_apps pipeline because this package already implements CSRF protection. In order to enable it, your host application must use the Plug.Session plug, which is usually configured in the endpoint module in Phoenix.

Mounted in another Plug application

Since it's just a plug, it can also be mounted into any other Plug application using Plug.Router.forward/2.

defmodule Another.App do
  use Plug.Router
  forward "/feature-flags", to: FunWithFlags.UI.Router, init_opts: [namespace: "feature-flags"]
end

Note: If your plug router uses Plug.CSRFProtection, FunWithFlags.UI.Router should be added before your CSRF protection plug because it already implements its own CSRF protection. If you declare FunWithFlags.UI.Router after, your CSRF plug will likely block GET requests for the JS assets of the dashboard.

Standalone

Again, because it's just a plug, it can be run standalone.

If you clone the repository, the library comes with two convenience functions to accomplish this:

# Simple, let Cowboy sort out the supervision tree:
{:ok, pid} = FunWithFlags.UI.run_standalone()

# Uses some explicit supervision configuration:
{:ok, pid} = FunWithFlags.UI.run_supervised()

These functions come in handy for local development, and are not necessary when embedding the Plug into a host application.

Please note that even though the FunWithFlags.UI module implements the Application behavior and comes with a proper start/2 callback, this is not enabled by design and, in fact, the Mixfile doesn't declare an entry module.

If you really need to run it standalone in a reliable manner, you are encouraged to write your own supervision setup.

Security

For obvious reasons, you don't want to make this web control panel publicly accessible.

The library itself doesn't provide any auth functionality because, as a Plug, it is easier to wrap it into the authentication and authorization logic of the host application.

The easiest thing to do is to protect it with HTTP Basic Auth, provided by Plug itself.

For example, in Phoenix:

defmodule MyPhoenixAppWeb.Router do
  use MyPhoenixAppWeb, :router
+ import Plug.BasicAuth

  pipeline :mounted_apps do
    plug :accepts, ["html"]
    plug :put_secure_browser_headers
+   plug :basic_auth, username: "foo", password: "bar"
  end

  scope path: "/feature-flags" do
    pipe_through :mounted_apps
    forward "/", FunWithFlags.UI.Router, namespace: "feature-flags"
  end
end

Caveats

While the base fun_with_flags library is quite relaxed in terms of valid flag names, group names and actor identifers, this web dashboard extension applies some more restrictive rules. The reason is that all fun_with_flags cares about is that some flag and group names can be represented as an Elixir Atom, while actor IDs are just strings. Since you can use that API in your code, the library will only check that the parameters have the right type.

Things change on the web, however. Think about the binary "Ook? Ook!". In code, it can be accepted as a valid flag name:

{:ok, true} = FunWithFlags.enable(:"Ook? Ook!", for_group: :"weird, huh?")

On the web, however, the question mark makes working with URLs a bit tricky: in http://localhost:8080/flags/Ook?%20Ook!, the flag name will be Ook and the rest will be a query string.

For this reason this library enforces some stricter rules when creating flags and groups. Blank values are not allowed, ? neither, and flag names must match /^w+$/.

Installation

The package can be installed by adding fun_with_flags_ui to your list of dependencies in mix.exs.
It requires fun_with_flags, see its installation documentation for more details.

def deps do
  [{:fun_with_flags_ui, "~> 0.8"}]
end

fun_with_flags_ui's People

Contributors

aturkewi avatar gazler avatar lostkobrakai avatar ryanwinchester avatar tiuipuv avatar tompave avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar

fun_with_flags_ui's Issues

(UndefinedFunctionError) function FunWithFlags.Store.Persistent.Redis.all_flags/0 is undefined (module FunWithFlags.Store.Persistent.Redis is not available)

Getting

Request: GET /feature-flags/flags
** (exit) an exception was raised:
    ** (UndefinedFunctionError) function FunWithFlags.Store.Persistent.Redis.all_flags/0 is undefined (module FunWithFlags.Store.Persistent.Redis is not available)
        FunWithFlags.Store.Persistent.Redis.all_flags()
        (fun_with_flags_ui 0.7.2) lib/fun_with_flags/ui/router.ex:73: anonymous fn/2 in FunWithFlags.UI.Router.do_match/4
        (fun_with_flags_ui 0.7.2) lib/plug/router.ex:280: anonymous fn/4 in FunWithFlags.UI.Router.dispatch/2
        (telemetry 0.4.3) /Users/q/Developer/app/deps/telemetry/src/telemetry.erl:272: :telemetry.span/3
        (fun_with_flags_ui 0.7.2) lib/plug/router.ex:276: FunWithFlags.UI.Router.dispatch/2
        (fun_with_flags_ui 0.7.2) lib/fun_with_flags/ui/router.ex:1: FunWithFlags.UI.Router.plug_builder_call/2
        (phoenix 1.5.13) lib/phoenix/router/route.ex:41: Phoenix.Router.Route.call/2
        (phoenix 1.5.13) lib/phoenix/router.ex:352: Phoenix.Router.__call__/2

for an app setup like this:

config :fun_with_flags, :persistence,
  adapter: FunWithFlags.Store.Persistent.Ecto,
  repo: App.Repo,
  ecto_table_name: "fun_with_flags_toggles"

config :fun_with_flags, :cache_bust_notifications, enabled: false
  pipeline :admins_only do
    plug :basic_auth, Application.compile_env(:app, :basic_auth)
  end

  scope path: "/feature-flags" do
    pipe_through [:mounted_apps, :admins_only]
    forward "/", FunWithFlags.UI.Router, namespace: "feature-flags"
  end
%{
  "fun_with_flags": {:hex, :fun_with_flags, "1.6.0", "507fcbc19374e83d34ba13d63b0816d37af952da1c6592978bbf40dad6a2e671", [:mix], [{:ecto_sql, "~> 3.0", [hex: :ecto_sql, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.0", [hex: :phoenix_pubsub, repo: "hexpm", optional: true]}, {:redix, "~> 1.0", [hex: :redix, repo: "hexpm", optional: true]}], "hexpm", "96b53f54737906c5a83b5d89922ca1145fda2d50db23b1f619478460b8d5f8d8"},
  "fun_with_flags_ui": {:hex, :fun_with_flags_ui, "0.7.2", "c8df9e90f92481c014824ab1ff5db7d501ac34ec28a4599b76251ec5a6db0861", [:mix], [{:cowboy, ">= 1.0.0", [hex: :cowboy, repo: "hexpm", optional: true]}, {:fun_with_flags, "~> 1.1", [hex: :fun_with_flags, repo: "hexpm", optional: false]}, {:plug, "~> 1.4", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, ">= 1.0.0", [hex: :plug_cowboy, repo: "hexpm", optional: true]}], "hexpm", "99b3635b067304722560ea3d5aab38ae23ea1217366c95ecb6263559488a22f2"},
}

How to select DB to read flags

Background

We are using this UI to check the flags we have on redis. Since Redis has several DBs one can choose from, this raises a problem - we need to tell the UI which Redis DB to use.

Problem

After reading the docs, I couldn't find any option to do that. By default, the UI will always read from Redis DB 1, which is not where our flags are.

Is there a way to tell the UI which Redis DB to use?

CSRF Protection Error when loading script

I'm now seeing a CSRF error when the details page tries to load the details.js script:

** (Plug.CSRFProtection.InvalidCrossOriginRequestError) security warning: an embedded <script> tag on another site requested protected JavaScript (if you know what you're doing, disable forgery protection for this route)

When this JS file does not load, there are no longer any confirmation boxes showing up.

I think we need to either ignore CSRF Protection on this route or add the token when loading up the script.

I introduced this error with PR #4 and I'll look at putting in a fix as soon as I can.

Provide bootstrap.min.css.gz unzipped

Similar topic was brought up here #8

What we have is a load balancer overriding the accept-encoding headers and as a result - bootstrap.css is not loaded.

Would you accept a pull request where both versions of the file are available: gzipped and not?

Persistent XSS Vulnerability

The FunWithflagsUI is vulnerable to persistent XSS attacks.

For Example a request in our Application like this:

POST /feature-flags/flags/some_flag/actors HTTP/1.1
_csrf_token=ZCoOGggmewMSFTxcFVkFMBU3PDITDC4n4ngbON8wMAeqYaDB-rVgwNXr&actor_id=aa<h1>bb&enabled=true

Results in page with the following HTML:

                <li class="list-group-item px-0">
                  <div id="actor_aa<h1>bb" class="container fwf-I-hate-grids">
  <div class="row no-gutters">
    <div class="col-lg-8 col-md-7 col-sm-5 col-3 text-left">
      <code>aa<h1>bb</code>
    </div>

Similarly GET paramaters can also generate XSS within the page:

GET /feature-flags/flags/some_other_flag#actor_cat:%3Ch1%3EXSSHERE HTTP/1.1

will get

<h1><code>XSSHERE</code>
    </h1>

All user inputs must be properly sanitised and encoded . See https://owasp.org/www-community/attacks/xss/ for more details about XSS

ArgumentError :erlang.binary_to_existing_atom/2

While trying to work with FunWithFlagsUI (especially with any new flag) from time to time we get such weird error:

ArgumentError :erlang.binary_to_existing_atom/2
errors were found at the given arguments: * 1st argument: not an already existing atom
lib/fun_with_flags/ui/router.ex in anonymous fn/3 in FunWithFlags.UI.Router.do_match/4 at line 99
lib/plug/router.ex in anonymous fn/4 in FunWithFlags.UI.Router.dispatch/2 at line 246
/opt/app/deps/telemetry/src/telemetry.erl in :telemetry.span/3 at line 321
lib/plug/router.ex in FunWithFlags.UI.Router.dispatch/2 at line 242
lib/fun_with_flags/ui/router.ex in FunWithFlags.UI.Router.plug_builder_call/2 at line 1
lib/phoenix/router/route.ex in Phoenix.Router.Route.call/2 at line 42
lib/phoenix/router.ex in Phoenix.Router.__call__/5 at line 425

I think it comes from there:

https://github.com/tompave/fun_with_flags_ui/blob/master/lib/fun_with_flags/ui/router.ex

defmodule FunWithFlags.UI.Router do
  ...
  get "/flags/:name" do
    case Utils.get_flag(name) do
      ...
    end
  end
  ...
end

https://github.com/tompave/fun_with_flags_ui/blob/master/lib/fun_with_flags/ui/utils.ex

defmodule FunWithFlags.UI.Utils do
  ...
  def get_flag(name) do
    ..
    case FunWithFlags.SimpleStore.lookup(String.to_existing_atom(name)) do
      ...
    end
    ...
  end
  ...
end

Maybe it's worth to replace String.to_existing_atom with String.to_atom to avoid the issue, what do you think?

Customize UI

Is there anyway of customizing the header to use a different color and text/logo?

Use conn.script_name instead of namespace to construct paths

I'm currently working on a similar "embed into some phoenix/plug app" tool than this one. In my research I found in bamboos preview router that they use conn.script_name to construct their routes. It would be nice to use this here as well, so the namespace option is no longer needed.

Can't use this library if using `fun_with_flags` with `runtime: false`

We use fun_with_flags as suggested in its README, that is:

  • runtime: false
  • We start FunWithFlags.Supervisor manually in our application's supervisor

If I try to use fun_with_flags_ui, this becomes problematic. fun_with_flags_ui lists fun_with_flags as a dependency without runtime: false. This means that the fun_with_flags application gets started anyways.

I think what we'd want to do in fun_with_flags_ui is:

  • List fun_with_flags as dependency with runtime: false
  • In fun_with_flags_ui's Application module, we manually start FunWithFlags.Supervisor

Thoughts?

Problem to load gzipped bootstrap in production

Hi,

In production, I'm not able to load the bootstrap gzipped file and Google Chrome is giving me this error message:

Refused to apply style from 'https://www.example.com/admin/feature-flags/assets/bootstrap.min.css'
because its MIME type ('') is not a supported stylesheet MIME type, and strict MIME checking
is enabled.

fwf

The style.css is working correctly, but the gzipped bootstrap don't. I couldn't find any Nginx config that was blocking it somehow. I guess I would have to add gzip_static on; for this location (didn't test though).

Wouldn't be better to load the file from bootstrap CDN instead? Like:

<link href="https://stackpath.bootstrapcdn.com/bootstrap/4.0.0-alpha.6/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-rwoIResjU2yc3z8GV/NPeZWAv56rSmLldC3R/AZzGRnGxQQKnKkoFVhFQhNUwEyJ" crossorigin="anonymous">

I appreciate any help :)

Flags with question marks are routed incorrectly

We have a check_email_verified? flag in our system which gets presented properly in the UI. But selecting it routes to feature-flags/flag/check_email_verified? so that the ? is interpreted as beginning of the query string.

This results in the flag not being found.

Current

Selecting a flag with a question mark (?) displays a "not found" details page instead of allowing to change the settings for the flag.

Expected

It's possible to see the detail page for flags with question marks (?) when selecting them on the index page.

Assets don't work when hosting with a subpath

We have a bit of a strange setup, where the app is hosted on a subpath. some.site.com/cool-app

It's also not a clever url rewrite, so the logic how to handle subpaths is a responsibility of the applications and the /cool-app part is actually in our router scopes. It is also part of our urls for static assets.

Now, this is a challenge when using fun_with_flags_ui. The assets are hardcoded and I can't inject the /cool-app prefix to them. Or can I, and I just don't know how?

I did a bit of research and looked how phoenix_live_dashboard does it, because the usage pattern is similar, but the assets work for them.

https://github.com/phoenixframework/phoenix_live_dashboard/blob/main/lib/phoenix/live_dashboard/controllers/assets.ex

They made a plug for serving static assets, so it works wherever it is mounted.

If this is the solution, I would be happy to come up with a PR.

Add support to protect from forgery

It looks like when I try and submit a form when piping through the :protect_from_forgery plug, the request fails due to lack of CSRF token.

It would be nice to be able to use this package with the proper CSRF protection.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.