Skip to content

Commit

Permalink
feat(admin) Add authorization provider in snuba admin (#2301)
Browse files Browse the repository at this point in the history
The admin relies on a proxy to perform authentication and authorization.
This is not great for two reasons:

it would by default allow everybody if the proxy was misconfigured while failing open (blocking everybody) would be safer.
it does not allow to decide which features to show depending on roles.
This adds an abstraction to performa authorization before each server request on the admin UI.
This abstraction has multiple implementations for multiple authorization providers. The default is NOOP and allows everything through. The next to be implemented is IAP, then we can add one for basic HTTP auth useful on prem.
  • Loading branch information
fpacifici authored Jan 4, 2022
1 parent 0f49805 commit 1a54e8a
Show file tree
Hide file tree
Showing 3 changed files with 58 additions and 1 deletion.
39 changes: 39 additions & 0 deletions snuba/admin/auth.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
from __future__ import annotations

from typing import Callable

from snuba import settings


class UnauthorizedException(Exception):
pass


auth_provider = Callable[[], str]

# This function takes the Flask request and authorizes it.
# If the request is valid it would return the user id.
# If not it will raise UnauthorizedException
#
# TODO: provide a more structured representation of the User that
# includes the role at least.
def authorize_request() -> str:
provider_id = settings.ADMIN_AUTH_PROVIDER
provider = AUTH_PROVIDERS.get(provider_id)
if provider is None:
raise ValueError("Invalid authorization provider")
return provider()


def passthrough_authorize() -> str:
return "unknown"


def iap_authorize() -> str:
raise NotImplementedError


AUTH_PROVIDERS = {
"NOOP": passthrough_authorize,
"IAP": iap_authorize,
}
18 changes: 17 additions & 1 deletion snuba/admin/views.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
from typing import Any, List, MutableMapping, Optional, cast

import simplejson as json
from flask import Flask, Response, jsonify, make_response, request
from flask import Flask, Response, g, jsonify, make_response, request

from snuba import state
from snuba.admin.auth import UnauthorizedException, authorize_request
from snuba.admin.clickhouse.nodes import get_storage_info
from snuba.admin.clickhouse.system_queries import (
InvalidCustomQuery,
Expand All @@ -29,6 +30,21 @@
USER_HEADER_KEY = "X-Goog-Authenticated-User-Email"


@application.errorhandler(UnauthorizedException)
def handle_invalid_json(exception: UnauthorizedException) -> Response:
return Response(
json.dumps({"error": "Unauthorized"}),
401,
{"Content-Type": "application/json"},
)


@application.before_request
def authorize() -> None:
user = authorize_request()
g.user = user


@application.route("/")
def root() -> Response:
return application.send_static_file("index.html")
Expand Down
2 changes: 2 additions & 0 deletions snuba/settings/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
ADMIN_HOST = os.environ.get("ADMIN_HOST", "0.0.0.0")
ADMIN_PORT = int(os.environ.get("ADMIN_PORT", 1219))

ADMIN_AUTH_PROVIDER = "NOOP"

ENABLE_DEV_FEATURES = os.environ.get("ENABLE_DEV_FEATURES", False)

DEFAULT_DATASET_NAME = "events"
Expand Down

0 comments on commit 1a54e8a

Please sign in to comment.