Skip to content

Commit

Permalink
Basic auth (#141)
Browse files Browse the repository at this point in the history
* Added BasicUpstream

* Added configuration

* Added integgration tests

* Addrtessed comments
  • Loading branch information
dalazx authored Mar 2, 2020
1 parent 318f6cd commit 2b37a38
Show file tree
Hide file tree
Showing 8 changed files with 329 additions and 67 deletions.
12 changes: 11 additions & 1 deletion platform_registry_api/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
from platform_registry_api.helpers import check_image_catalog_permission

from .aws_ecr import AWSECRUpstream
from .basic import BasicUpstream
from .config import Config, EnvironConfigFactory, UpstreamRegistryConfig
from .oauth import OAuthClient, OAuthUpstream
from .typedefs import TimeFactory
Expand Down Expand Up @@ -463,6 +464,13 @@ def _convert_location_header(self, url_str: str, url_factory: URLFactory) -> str
return str(registry_repo_url.url)


@asynccontextmanager
async def create_basic_upstream(
*, config: UpstreamRegistryConfig
) -> AsyncIterator[Upstream]:
yield BasicUpstream(username=config.basic_username, password=config.basic_password)


@asynccontextmanager
async def create_oauth_upstream(
*, config: UpstreamRegistryConfig, client: aiohttp.ClientSession
Expand Down Expand Up @@ -533,7 +541,9 @@ async def on_request_redirect(session, ctx, params):
)
app["v2_app"]["registry_client"] = session

if config.upstream_registry.is_oauth:
if config.upstream_registry.is_basic:
upstream_cm = create_basic_upstream(config=config.upstream_registry)
elif config.upstream_registry.is_oauth:
upstream_cm = create_oauth_upstream(
config=config.upstream_registry, client=session
)
Expand Down
25 changes: 25 additions & 0 deletions platform_registry_api/basic.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
from typing import Dict

from aiohttp import BasicAuth
from aiohttp.hdrs import AUTHORIZATION

from .upstream import Upstream


class BasicUpstream(Upstream):
def __init__(self, *, username: str, password: str) -> None:
self._username = username
self._password = password

async def _get_headers(self) -> Dict[str, str]:
auth = BasicAuth(login=self._username, password=self._password)
return {str(AUTHORIZATION): auth.encode()}

async def get_headers_for_version(self) -> Dict[str, str]:
return await self._get_headers()

async def get_headers_for_catalog(self) -> Dict[str, str]:
return await self._get_headers()

async def get_headers_for_repo(self, repo: str) -> Dict[str, str]:
return await self._get_headers()
15 changes: 15 additions & 0 deletions platform_registry_api/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ class AuthConfig:


class UpstreamType(str, Enum):
BASIC = "basic"
OAUTH = "oauth"
AWS_ECR = "aws_ecr"

Expand All @@ -31,6 +32,9 @@ class UpstreamRegistryConfig:

type: UpstreamType = UpstreamType.OAUTH

basic_username: str = field(repr=False, default="")
basic_password: str = field(repr=False, default="")

# TODO: should be derived from the WWW-Authenticate header instead
token_endpoint_url: URL = URL()
token_service: str = ""
Expand All @@ -45,6 +49,10 @@ class UpstreamRegistryConfig:
# https://github.com/docker/distribution/blob/dcfe05ce6cff995f419f8df37b59987257ffb8c1/registry/handlers/catalog.go#L16
max_catalog_entries: int = 100

@property
def is_basic(self) -> bool:
return self.type == UpstreamType.BASIC

@property
def is_oauth(self) -> bool:
return self.type == UpstreamType.OAUTH
Expand Down Expand Up @@ -115,6 +123,13 @@ def create_upstream_registry(self) -> UpstreamRegistryConfig:
upstream["token_repository_scope_actions"] = self._environ[
"NP_REGISTRY_UPSTREAM_TOKEN_REPO_SCOPE_ACTIONS"
]
if upstream_type == UpstreamType.BASIC:
basic_username = self._environ.get("NP_REGISTRY_UPSTREAM_BASIC_USERNAME")
if basic_username is not None:
upstream["basic_username"] = basic_username
basic_password = self._environ.get("NP_REGISTRY_UPSTREAM_BASIC_PASSWORD")
if basic_password is not None:
upstream["basic_password"] = basic_password
return UpstreamRegistryConfig(**upstream) # type: ignore

def create_auth(self) -> AuthConfig:
Expand Down
63 changes: 63 additions & 0 deletions tests/integration/conftest.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
import os
import uuid
from dataclasses import dataclass
from typing import Optional

import pytest
from aiohttp import BasicAuth
from jose import jwt
from neuro_auth_client import AuthClient, User


@pytest.fixture
Expand All @@ -12,6 +18,63 @@ def event_loop(loop):
return loop


@pytest.fixture
def cluster_name():
return "test-cluster"


@dataclass
class _User:
name: str
token: str

def to_basic_auth(self) -> BasicAuth:
return BasicAuth(login=self.name, password=self.token) # type: ignore


@pytest.fixture
async def auth_client(config, admin_token):
async with AuthClient(
url=config.auth.server_endpoint_url, token=admin_token
) as client:
yield client


@pytest.fixture
async def regular_user_factory(auth_client, token_factory, admin_token, cluster_name):
async def _factory(name: Optional[str] = None) -> User:
if not name:
name = str(uuid.uuid4())
user = User(name=name)
await auth_client.add_user(user)
# Grant permissions to the user images
headers = auth_client._generate_headers(admin_token)
payload = [
{"uri": f"image://{cluster_name}/{name}", "action": "manage"},
]
async with auth_client._request(
"POST", f"/api/v1/users/{name}/permissions", headers=headers, json=payload
) as p:
assert p.status == 201
return _User(name=user.name, token=token_factory(user.name)) # type: ignore

return _factory


@pytest.fixture(scope="session")
def in_docker():
return os.path.isfile("/.dockerenv")


@pytest.fixture
def token_factory():
def _factory(name: str):
payload = {"identity": name}
return jwt.encode(payload, "secret", algorithm="HS256")

return _factory


@pytest.fixture
def admin_token(token_factory):
return token_factory("admin")
68 changes: 2 additions & 66 deletions tests/integration/test_api.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,4 @@
import uuid
from dataclasses import dataclass
from typing import Optional

import pytest
from aiohttp import BasicAuth
from jose import jwt
from neuro_auth_client import AuthClient, User
from yarl import URL

from platform_registry_api.api import create_app
Expand All @@ -20,26 +13,7 @@


@pytest.fixture
def token_factory():
def _factory(name: str):
payload = {"identity": name}
return jwt.encode(payload, "secret", algorithm="HS256")

return _factory


@pytest.fixture
def admin_token(token_factory):
return token_factory("admin")


@pytest.fixture
def cluster_name():
return "test-cluster"


@pytest.fixture
def config(in_docker, admin_token):
def config(in_docker, admin_token, cluster_name):
if in_docker:
return EnvironConfigFactory().create()

Expand All @@ -60,48 +34,10 @@ def config(in_docker, admin_token):
upstream_registry=upstream_registry,
auth=auth,
zipkin=zipkin_config,
cluster_name="test-cluster",
cluster_name=cluster_name,
)


@dataclass
class _User:
name: str
token: str

def to_basic_auth(self) -> BasicAuth:
return BasicAuth(login=self.name, password=self.token) # type: ignore


@pytest.fixture
async def auth_client(config, admin_token):
async with AuthClient(
url=config.auth.server_endpoint_url, token=admin_token
) as client:
yield client


@pytest.fixture
async def regular_user_factory(auth_client, token_factory, admin_token, cluster_name):
async def _factory(name: Optional[str] = None) -> User:
if not name:
name = str(uuid.uuid4())
user = User(name=name)
await auth_client.add_user(user)
# Grant permissions to the user images
headers = auth_client._generate_headers(admin_token)
payload = [
{"uri": f"image://{cluster_name}/{name}", "action": "manage"},
]
async with auth_client._request(
"POST", f"/api/v1/users/{name}/permissions", headers=headers, json=payload
) as p:
assert p.status == 201
return _User(name=user.name, token=token_factory(user.name)) # type: ignore

return _factory


class TestV2Api:
@pytest.mark.asyncio
async def test_unauthorized(self, aiohttp_client, config):
Expand Down
107 changes: 107 additions & 0 deletions tests/integration/test_basic.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
from contextlib import asynccontextmanager
from typing import AsyncIterator

import aiohttp.web
import pytest
from aiohttp import BasicAuth, hdrs, web
from aiohttp.test_utils import unused_port
from aiohttp.web import Application, HTTPOk, Request, Response, json_response
from yarl import URL

from platform_registry_api.api import create_app
from platform_registry_api.config import (
AuthConfig,
Config,
EnvironConfigFactory,
ServerConfig,
UpstreamRegistryConfig,
UpstreamType,
ZipkinConfig,
)


pytestmark = pytest.mark.asyncio


@pytest.fixture
async def raw_client() -> AsyncIterator[aiohttp.ClientSession]:
async with aiohttp.ClientSession() as session:
yield session


@asynccontextmanager
async def create_local_app_server(
app: aiohttp.web.Application, host: str = "0.0.0.0", port: int = 8080
) -> AsyncIterator[URL]:
runner = aiohttp.web.AppRunner(app)
try:
await runner.setup()
site = aiohttp.web.TCPSite(runner, host, port)
await site.start()
yield URL(site.name)
finally:
await runner.shutdown()
await runner.cleanup()


class _TestUpstreamHandler:
async def handle_catalog(self, request: Request) -> Response:
auth_header_value = request.headers[hdrs.AUTHORIZATION]
assert BasicAuth.decode(auth_header_value) == BasicAuth(
login="testuser", password="testpassword"
)
return json_response({"repositories": []})


@pytest.fixture
def handler() -> _TestUpstreamHandler:
return _TestUpstreamHandler()


@pytest.fixture
async def upstream(handler: _TestUpstreamHandler) -> AsyncIterator[URL]:
app = Application()
app.add_routes([web.get("/v2/_catalog", handler.handle_catalog)])

async with create_local_app_server(app, port=unused_port()) as url:
yield url


@pytest.fixture
def auth_config(in_docker: bool, admin_token: str) -> AuthConfig:
if in_docker:
return EnvironConfigFactory().create().auth
return AuthConfig(
server_endpoint_url=URL("http://localhost:5003"), service_token=admin_token
)


@pytest.fixture
def config(upstream: URL, auth_config: AuthConfig) -> Config:
upstream_registry = UpstreamRegistryConfig(
type=UpstreamType.BASIC,
endpoint_url=upstream,
project="testproject",
basic_username="testuser",
basic_password="testpassword",
)
zipkin_config = ZipkinConfig(URL("http://zipkin:9411"), 0)
return Config(
server=ServerConfig(),
upstream_registry=upstream_registry,
auth=auth_config,
zipkin=zipkin_config,
cluster_name="test-cluster",
)


class TestBasicUpstream:
async def test_catalog(self, config, regular_user_factory, aiohttp_client) -> None:
app = await create_app(config)
client = await aiohttp_client(app)
user = await regular_user_factory()

async with client.get("/v2/_catalog", auth=user.to_basic_auth()) as resp:
assert resp.status == HTTPOk.status_code, await resp.text()
payload = await resp.json()
assert payload == {"repositories": []}
Loading

0 comments on commit 2b37a38

Please sign in to comment.