Ejemplo n.º 1
0
 def _valid_bootstrap_token(cls, v: Optional[str]) -> Optional[str]:
     if not v:
         return None
     try:
         Token.from_str(v)
         return v
     except Exception as e:
         raise ValueError(f"bootstrap_token not a valid token: {str(e)}")
Ejemplo n.º 2
0
async def test_token_info(driver: webdriver.Chrome,
                          selenium_config: SeleniumConfig) -> None:
    cookie = await State(token=selenium_config.token).as_cookie()

    # Create a notebook token and an internal token.
    r = httpx.get(
        urljoin(selenium_config.url, "/auth"),
        params={
            "scope": "exec:test",
            "notebook": "true"
        },
        headers={"Cookie": f"{COOKIE_NAME}={cookie}"},
    )
    assert r.status_code == 200
    notebook_token = Token.from_str(r.headers["X-Auth-Request-Token"])
    r = httpx.get(
        urljoin(selenium_config.url, "/auth"),
        params={
            "scope": "exec:test",
            "delegate_to": "service"
        },
        headers={"Cookie": f"{COOKIE_NAME}={cookie}"},
    )
    assert r.status_code == 200
    internal_token = Token.from_str(r.headers["X-Auth-Request-Token"])

    # Load the token page and go to the history for our session token.
    driver.get(urljoin(selenium_config.url, "/auth/tokens"))
    tokens_page = TokensPage(driver)
    session_tokens = tokens_page.get_tokens(TokenType.session)
    session_token = next(t for t in session_tokens
                         if t.token == selenium_config.token.key)
    session_token.click_token()

    # We should now be at the token information page for the session token.
    data_page = TokenDataPage(driver)
    assert data_page.username == "testuser"
    assert data_page.token_type == "session"
    scopes = sorted(selenium_config.config.known_scopes.keys())
    assert data_page.scopes == ", ".join(scopes)
    history = data_page.get_change_history()
    assert len(history) == 3
    assert history[0].action == "create"
    assert history[0].token == internal_token.key
    assert history[0].scopes == ""
    assert history[1].action == "create"
    assert history[1].token == notebook_token.key
    assert history[1].scopes == ", ".join(scopes)
    assert history[2].action == "create"
    assert history[2].token == selenium_config.token.key
    assert history[2].scopes == ", ".join(scopes)
Ejemplo n.º 3
0
async def token_data_from_secret(token_service: TokenService,
                                 secret: V1Secret) -> TokenData:
    assert "token" in secret.data
    token = b64decode(secret.data["token"].encode()).decode()
    data = await token_service.get_data(Token.from_str(token))
    assert data
    return data
Ejemplo n.º 4
0
async def post_analyze(
    token_str: str = Form(
        ...,
        alias="token",
        title="Token to analyze",
        example="gt-db59fbkT5LrGHvhLMglNWw.G3NEmhWZr8JwO8AQ8sIWpQ",
    ),
    context: RequestContext = Depends(context_dependency),
) -> Dict[str, Dict[str, Any]]:
    """Analyze a token.

    Expects a POST with a single parameter, ``token``, containing the token.
    Returns a JSON structure with details about that token.
    """
    try:
        token = Token.from_str(token_str)
    except InvalidTokenError as e:
        return {"token": {"errors": [str(e)], "valid": False}}

    token_service = context.factory.create_token_service()
    token_data = await token_service.get_data(token)
    if not token_data:
        return {
            "handle": token.dict(),
            "token": {
                "errors": ["Invalid token"],
                "valid": False
            },
        }
    result = token_data_to_analysis(token_data)
    result["handle"] = token.dict()
    return result
Ejemplo n.º 5
0
async def test_internal(setup: SetupTest) -> None:
    token_data = await setup.create_session_token(
        group_names=["admin"], scopes=["exec:admin", "read:all", "read:some"])
    assert token_data.expires

    r = await setup.client.get(
        "/auth",
        params={
            "scope": "exec:admin",
            "delegate_to": "a-service",
            "delegate_scope": " read:some  ,read:all  ",
        },
        headers={"Authorization": f"Bearer {token_data.token}"},
    )
    assert r.status_code == 200
    internal_token = Token.from_str(r.headers["X-Auth-Request-Token"])
    assert internal_token != token_data.token

    r = await setup.client.get(
        "/auth/api/v1/token-info",
        headers={"Authorization": f"Bearer {internal_token}"},
    )
    assert r.status_code == 200
    assert r.json() == {
        "token": internal_token.key,
        "username": token_data.username,
        "token_type": "internal",
        "scopes": ["read:all", "read:some"],
        "service": "a-service",
        "created": ANY,
        "expires": int(token_data.expires.timestamp()),
        "parent": token_data.token.key,
    }

    # Requesting a token with the same parameters returns the same token.
    r = await setup.client.get(
        "/auth",
        params={
            "scope": "exec:admin",
            "delegate_to": "a-service",
            "delegate_scope": "read:all,read:some",
        },
        headers={"Authorization": f"Bearer {token_data.token}"},
    )
    assert r.status_code == 200
    assert internal_token == Token.from_str(r.headers["X-Auth-Request-Token"])
Ejemplo n.º 6
0
async def test_internal(client: AsyncClient,
                        factory: ComponentFactory) -> None:
    data = await create_session_token(factory,
                                      scopes=["exec:test", "read:all"])
    await set_session_cookie(client, data.token)

    request_awaits = []
    for _ in range(100):
        request_awaits.append(
            client.get(
                "/auth",
                params={
                    "scope": "exec:test",
                    "delegate_to": "a-service",
                    "delegate_scope": "read:all",
                },
            ))
    responses = await asyncio.gather(*request_awaits)
    assert responses[0].status_code == 200
    token = Token.from_str(responses[0].headers["X-Auth-Request-Token"])
    for r in responses:
        assert r.status_code == 200
        assert Token.from_str(r.headers["X-Auth-Request-Token"]) == token

    request_awaits = []
    for _ in range(100):
        request_awaits.append(
            client.get(
                "/auth",
                params={
                    "scope": "exec:test",
                    "delegate_to": "a-service",
                    "delegate_scope": "exec:test",
                },
            ))
    responses = await asyncio.gather(*request_awaits)
    assert responses[0].status_code == 200
    new_token = Token.from_str(responses[0].headers["X-Auth-Request-Token"])
    assert new_token != token
    for r in responses:
        assert r.status_code == 200
        assert Token.from_str(r.headers["X-Auth-Request-Token"]) == new_token
Ejemplo n.º 7
0
async def run_app(tmp_path: Path,
                  settings_path: Path) -> AsyncIterator[SeleniumConfig]:
    """Run the application as a separate process for Selenium access.

    Parameters
    ----------
    tmp_path : `pathlib.Path`
        The temporary directory for testing.
    settings_path : `pathlib.Path`
        The path to the settings file.
    """
    config_dependency.set_settings_path(str(settings_path))
    config = await config_dependency()
    token_path = tmp_path / "token"

    # Create the socket that the app will listen on.
    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    s.bind(("127.0.0.1", 0))
    port = s.getsockname()[1]

    # Spawn the app in a separate process using uvicorn.
    cmd = [
        "uvicorn",
        "--fd",
        "0",
        "--factory",
        "tests.support.selenium:create_app",
    ]
    logging.info("Starting server with command %s", " ".join(cmd))
    p = subprocess.Popen(
        cmd,
        cwd=str(tmp_path),
        stdin=s.fileno(),
        env={
            **os.environ,
            "GAFAELFAWR_SETTINGS_PATH": str(settings_path),
            "GAFAELFAWR_TEST_TOKEN_PATH": str(token_path),
            "PYTHONPATH": os.getcwd(),
        },
    )
    s.close()

    logging.info("Waiting for server to start")
    _wait_for_server(port)

    try:
        selenium_config = SeleniumConfig(
            config=config,
            token=Token.from_str(token_path.read_text()),
            url=f"http://localhost:{port}",
        )
        yield selenium_config
    finally:
        p.terminate()
Ejemplo n.º 8
0
async def test_no_expires(
    client: AsyncClient, factory: ComponentFactory
) -> None:
    """Test creating a user token that doesn't expire."""
    token_data = await create_session_token(factory)
    csrf = await set_session_cookie(client, token_data.token)

    r = await client.post(
        f"/auth/api/v1/users/{token_data.username}/tokens",
        headers={"X-CSRF-Token": csrf},
        json={"token_name": "some token"},
    )
    assert r.status_code == 201
    token_url = r.headers["Location"]

    r = await client.get(token_url)
    assert "expires" not in r.json()

    # Create a user token with an expiration and then adjust it to not expire.
    now = datetime.now(tz=timezone.utc).replace(microsecond=0)
    expires = now + timedelta(days=2)
    r = await client.post(
        f"/auth/api/v1/users/{token_data.username}/tokens",
        headers={"X-CSRF-Token": csrf},
        json={
            "token_name": "another token",
            "expires": int(expires.timestamp()),
        },
    )
    assert r.status_code == 201
    user_token = Token.from_str(r.json()["token"])
    token_service = factory.create_token_service()
    user_token_data = await token_service.get_data(user_token)
    assert user_token_data and user_token_data.expires == expires
    token_url = r.headers["Location"]

    r = await client.get(token_url)
    assert r.json()["expires"] == int(expires.timestamp())

    r = await client.patch(
        token_url,
        headers={"X-CSRF-Token": csrf},
        json={"expires": None},
    )
    assert r.status_code == 201
    assert "expires" not in r.json()

    # Check that the expiration was also changed in Redis.
    user_token_data = await token_service.get_data(user_token)
    assert user_token_data and user_token_data.expires is None
Ejemplo n.º 9
0
    async def _update_service_secret(
        self, service_secret: ServiceSecret
    ) -> None:
        """Verify that a service secret is still correct.

        This checks that the service token is still valid and replaces it with
        a new one if not.
        """
        name = service_secret.secret_name
        namespace = service_secret.secret_namespace
        try:
            secret = self._storage.get_secret(
                name, namespace, SecretType.service
            )
        except KubernetesError as e:
            msg = f"Updating {namespace}/{name} failed"
            self._logger.error(msg, error=str(e))
            return
        if not secret:
            self._logger.error(
                f"Updating {namespace}/{name} failed",
                error=f"Secret {namespace}/{name} not found while updating",
            )
            return

        valid = False
        if "token" in secret.data:
            try:
                token_str = b64decode(secret.data["token"]).decode()
                token = Token.from_str(token_str)
                valid = await self._check_service_token(token, service_secret)
            except Exception:
                valid = False
        if valid:
            return

        # The token is not valid.  Replace the secret.
        token = await self._create_service_token(service_secret)
        try:
            self._storage.patch_secret(name, namespace, token)
        except KubernetesError as e:
            msg = f"Updating {namespace}/{name} failed"
            self._logger.error(msg, error=str(e))
        else:
            self._logger.info(
                f"Updated {namespace}/{name} secret",
                service=service_secret.service,
                scopes=service_secret.scopes,
            )
Ejemplo n.º 10
0
def test_token_from_str() -> None:
    bad_tokens = [
        "",
        ".",
        "MLF5MB3Peg79wEC0BY8U8Q",
        "MLF5MB3Peg79wEC0BY8U8Q.",
        "gt-",
        "gt-.",
        "gt-MLF5MB3Peg79wEC0BY8U8Q",
        "gt-MLF5MB3Peg79wEC0BY8U8Q.",
        "gt-.ChbkqEyp3EIJ2e_1Sqff3w",
        "gt-NOT.VALID",
        "gt-MLF5MB3Peg79wEC0BY8U8Q.ChbkqEyp3EIJ2e_1Sqff3w.!!!!",
        "gtMLF5MB3Peg79wEC0BY8U8Q.ChbkqEyp3EIJ2e_1Sqff3w",
    ]
    for token_str in bad_tokens:
        with pytest.raises(InvalidTokenError):
            Token.from_str(token_str)

    token_str = "gt-MLF5MB3Peg79wEC0BY8U8Q.ChbkqEyp3EIJ2e_1Sqff3w"
    token = Token.from_str(token_str)
    assert token.key == "MLF5MB3Peg79wEC0BY8U8Q"
    assert token.secret == "ChbkqEyp3EIJ2e_1Sqff3w"
    assert str(token) == token_str
Ejemplo n.º 11
0
def run_app(tmp_path: Path, settings_path: Path) -> Iterator[SeleniumConfig]:
    """Run the application as a separate process for Selenium access.

    Parameters
    ----------
    tmp_path : `pathlib.Path`
        The temporary directory for testing.
    settings_path : `pathlib.Path`
        The path to the settings file.
    """
    config_dependency.set_settings_path(str(settings_path))
    config = config_dependency()
    initialize_database(config)

    token_path = tmp_path / "token"
    app_source = APP_TEMPLATE.format(
        settings_path=str(settings_path),
        token_path=str(token_path),
    )
    app_path = tmp_path / "testing.py"
    with app_path.open("w") as f:
        f.write(app_source)

    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    s.bind(("127.0.0.1", 0))
    port = s.getsockname()[1]

    cmd = ["uvicorn", "--fd", "0", "testing:app"]
    logging.info("Starting server with command %s", " ".join(cmd))
    p = subprocess.Popen(cmd, cwd=str(tmp_path), stdin=s.fileno())
    s.close()

    logging.info("Waiting for server to start")
    _wait_for_server(port)

    try:
        selenium_config = SeleniumConfig(
            token=Token.from_str(token_path.read_text()),
            url=f"http://localhost:{port}",
        )
        yield selenium_config
    finally:
        p.terminate()
Ejemplo n.º 12
0
    def from_cookie(cls, cookie: str, request: Optional[Request]) -> State:
        """Reconstruct state from an encrypted cookie.

        Parameters
        ----------
        cookie : `str`
            The encrypted cookie value.
        key : `bytes`
            The `~cryptography.fernet.Fernet` key used to decrypt it.
        request : `fastapi.Request` or `None`
            The request, used for logging.  If not provided (primarily for the
            test suite), invalid state cookies will not be logged.

        Returns
        -------
        state : `State`
            The state represented by the cookie.
        """
        key = config_dependency().session_secret.encode()
        fernet = Fernet(key)
        try:
            data = json.loads(fernet.decrypt(cookie.encode()).decode())
            token = None
            if "token" in data:
                token = Token.from_str(data["token"])
        except Exception as e:
            if request:
                logger = get_logger(request)
                logger.warning("Discarding invalid state cookie", error=str(e))
            return cls()

        return cls(
            csrf=data.get("csrf"),
            token=token,
            return_url=data.get("return_url"),
            state=data.get("state"),
        )
Ejemplo n.º 13
0
async def test_create_delete_modify(
    client: AsyncClient, factory: ComponentFactory, caplog: LogCaptureFixture
) -> None:
    user_info = TokenUserInfo(
        username="******",
        name="Example Person",
        email="*****@*****.**",
        uid=45613,
        groups=[TokenGroup(name="foo", id=12313)],
    )
    token_service = factory.create_token_service()
    async with factory.session.begin():
        session_token = await token_service.create_session_token(
            user_info,
            scopes=["read:all", "exec:admin", "user:token"],
            ip_address="127.0.0.1",
        )
    csrf = await set_session_cookie(client, session_token)

    expires = current_datetime() + timedelta(days=100)
    r = await client.post(
        "/auth/api/v1/users/example/tokens",
        headers={"X-CSRF-Token": csrf},
        json={
            "token_name": "some token",
            "scopes": ["read:all"],
            "expires": int(expires.timestamp()),
        },
    )
    assert r.status_code == 201
    assert r.json() == {"token": ANY}
    user_token = Token.from_str(r.json()["token"])
    token_url = r.headers["Location"]
    assert token_url == f"/auth/api/v1/users/example/tokens/{user_token.key}"

    r = await client.get(token_url)
    assert r.status_code == 200
    info = r.json()
    assert info == {
        "token": user_token.key,
        "username": "******",
        "token_name": "some token",
        "token_type": "user",
        "scopes": ["read:all"],
        "created": ANY,
        "expires": int(expires.timestamp()),
    }

    # Check that this is the same information as is returned by the token-info
    # route.  This is a bit tricky to do since the cookie will take precedence
    # over the Authorization header, but we can't just delete the cookie since
    # we'll lose the CSRF token.  Save the cookie and delete it, and then
    # later restore it.
    cookie = client.cookies.pop(COOKIE_NAME)
    r = await client.get(
        "/auth/api/v1/token-info",
        headers={"Authorization": f"bearer {user_token}"},
    )
    assert r.status_code == 200
    assert r.json() == info
    client.cookies.set(COOKIE_NAME, cookie, domain=TEST_HOSTNAME)

    # Listing all tokens for this user should return the user token and a
    # session token.
    r = await client.get("/auth/api/v1/users/example/tokens")
    assert r.status_code == 200
    data = r.json()

    # Adjust for sorting, which will be by creation date and then token.
    assert len(data) == 2
    if data[0] == info:
        session_info = data[1]
    else:
        assert data[1] == info
        session_info = data[0]
    assert session_info == {
        "token": session_token.key,
        "username": "******",
        "token_type": "session",
        "scopes": ["exec:admin", "read:all", "user:token"],
        "created": ANY,
        "expires": ANY,
    }

    # Change the name, scope, and expiration of the token.
    caplog.clear()
    new_expires = current_datetime() + timedelta(days=200)
    r = await client.patch(
        token_url,
        headers={"X-CSRF-Token": csrf},
        json={
            "token_name": "happy token",
            "scopes": ["exec:admin"],
            "expires": int(new_expires.timestamp()),
        },
    )
    assert r.status_code == 201
    assert r.json() == {
        "token": user_token.key,
        "username": "******",
        "token_name": "happy token",
        "token_type": "user",
        "scopes": ["exec:admin"],
        "created": ANY,
        "expires": int(new_expires.timestamp()),
    }

    # Check the logging.  Regression test for a bug where new expirations
    # would be logged as raw datetime objects instead of timestamps.
    assert parse_log(caplog) == [
        {
            "expires": int(new_expires.timestamp()),
            "event": "Modified token",
            "httpRequest": {
                "requestMethod": "PATCH",
                "requestUrl": f"https://{TEST_HOSTNAME}{token_url}",
                "remoteIp": "127.0.0.1",
            },
            "key": user_token.key,
            "scope": "exec:admin read:all user:token",
            "severity": "info",
            "token": session_token.key,
            "token_name": "happy token",
            "token_scope": "exec:admin",
            "token_source": "cookie",
            "user": "******",
        }
    ]

    # Delete the token.
    r = await client.delete(token_url, headers={"X-CSRF-Token": csrf})
    assert r.status_code == 204
    r = await client.get(token_url)
    assert r.status_code == 404

    # Deleting again should return 404.
    r = await client.delete(token_url, headers={"X-CSRF-Token": csrf})
    assert r.status_code == 404

    # This user should now have only one token.
    r = await client.get("/auth/api/v1/users/example/tokens")
    assert r.status_code == 200
    assert len(r.json()) == 1

    # We should be able to see the change history for the token.
    r = await client.get(token_url + "/change-history")
    assert r.status_code == 200
    assert r.json() == [
        {
            "token": user_token.key,
            "username": "******",
            "token_type": "user",
            "token_name": "happy token",
            "scopes": ["exec:admin"],
            "expires": int(new_expires.timestamp()),
            "actor": "example",
            "action": "revoke",
            "ip_address": "127.0.0.1",
            "event_time": ANY,
        },
        {
            "token": user_token.key,
            "username": "******",
            "token_type": "user",
            "token_name": "happy token",
            "scopes": ["exec:admin"],
            "expires": int(new_expires.timestamp()),
            "actor": "example",
            "action": "edit",
            "old_token_name": "some token",
            "old_scopes": ["read:all"],
            "old_expires": int(expires.timestamp()),
            "ip_address": "127.0.0.1",
            "event_time": ANY,
        },
        {
            "token": user_token.key,
            "username": "******",
            "token_type": "user",
            "token_name": "some token",
            "scopes": ["read:all"],
            "expires": int(expires.timestamp()),
            "actor": "example",
            "action": "create",
            "ip_address": "127.0.0.1",
            "event_time": ANY,
        },
    ]
Ejemplo n.º 14
0
def test_generate_token() -> None:
    runner = CliRunner()
    result = runner.invoke(main, ["generate-token"])

    assert result.exit_code == 0
    assert Token.from_str(result.output.rstrip("\n"))
Ejemplo n.º 15
0
    def from_file(cls, path: str) -> Config:
        """Construct a Config object from a settings file.

        Parameters
        ----------
        path : `str`
            Path to the settings file in YAML.

        Returns
        -------
        config : `Config`
            The corresponding Config object.
        """
        with open(path, "r") as f:
            raw_settings = yaml.safe_load(f)
        settings = Settings.parse_obj(raw_settings)

        # Load the secrets from disk.
        key = cls._load_secret(settings.issuer.key_file)
        keypair = RSAKeyPair.from_pem(key)
        session_secret = cls._load_secret(settings.session_secret_file)
        redis_password = None
        if settings.redis_password_file:
            path = settings.redis_password_file
            redis_password = cls._load_secret(path).decode()
        influxdb_secret = None
        if settings.issuer.influxdb_secret_file:
            path = settings.issuer.influxdb_secret_file
            influxdb_secret = cls._load_secret(path).decode()
        if settings.github:
            path = settings.github.client_secret_file
            github_secret = cls._load_secret(path).decode()
        if settings.oidc:
            path = settings.oidc.client_secret_file
            oidc_secret = cls._load_secret(path).decode()

        # The database URL may have a separate secret in database_password, in
        # which case it needs to be added to the URL.
        database_url = settings.database_url
        if settings.database_password:
            parsed_url = urlparse(database_url)
            database_password = settings.database_password.get_secret_value()
            database_netloc = (f"{parsed_url.username}:{database_password}"
                               f"@{parsed_url.hostname}")
            database_url = parsed_url._replace(netloc=database_netloc).geturl()

        # If there is an OpenID Connect server configuration, load it from a
        # file in JSON format.  (It contains secrets.)
        oidc_server_config = None
        if settings.oidc_server_secrets_file:
            path = settings.oidc_server_secrets_file
            oidc_secrets_json = cls._load_secret(path).decode()
            oidc_secrets = json.loads(oidc_secrets_json)
            oidc_clients = tuple((OIDCClient(client_id=c["id"],
                                             client_secret=c["secret"])
                                  for c in oidc_secrets))
            oidc_server_config = OIDCServerConfig(clients=oidc_clients)

        # The group mapping in the settings maps a scope to a list of groups
        # that provide that scope.  This may be conceptually easier for the
        # person writing the configuration, but for our purposes we want a map
        # from a group name to a set of scopes that group provides.
        #
        # Reconstruct the group mapping in the form in which we want to use it
        # internally.
        group_mapping = defaultdict(set)
        for scope, groups in settings.group_mapping.items():
            for group in groups:
                group_mapping[group].add(scope)
        group_mapping_frozen = {
            k: frozenset(v)
            for k, v in group_mapping.items()
        }

        # Build the Config object.
        bootstrap_token = None
        if settings.bootstrap_token:
            bootstrap_token = Token.from_str(settings.bootstrap_token)
        issuer_config = IssuerConfig(
            iss=settings.issuer.iss,
            kid=settings.issuer.key_id,
            aud=settings.issuer.aud,
            keypair=keypair,
            exp_minutes=settings.issuer.exp_minutes,
            group_mapping=group_mapping_frozen,
            username_claim=settings.username_claim,
            uid_claim=settings.uid_claim,
            influxdb_secret=influxdb_secret,
            influxdb_username=settings.issuer.influxdb_username,
        )
        verifier_config = VerifierConfig(
            iss=settings.issuer.iss,
            aud=settings.issuer.aud,
            keypair=keypair,
            username_claim=settings.username_claim,
            uid_claim=settings.uid_claim,
            oidc_iss=settings.oidc.issuer if settings.oidc else None,
            oidc_aud=settings.oidc.audience if settings.oidc else None,
            oidc_kids=tuple(settings.oidc.key_ids if settings.oidc else []),
        )
        github_config = None
        if settings.github:
            github_config = GitHubConfig(
                client_id=settings.github.client_id,
                client_secret=github_secret,
                username_claim=settings.username_claim,
                uid_claim=settings.uid_claim,
            )
        oidc_config = None
        if settings.oidc:
            oidc_config = OIDCConfig(
                client_id=settings.oidc.client_id,
                client_secret=oidc_secret,
                login_url=str(settings.oidc.login_url),
                login_params=settings.oidc.login_params,
                redirect_url=str(settings.oidc.redirect_url),
                token_url=str(settings.oidc.token_url),
                scopes=tuple(settings.oidc.scopes),
                issuer=settings.oidc.issuer,
                audience=settings.oidc.audience,
                key_ids=tuple(settings.oidc.key_ids),
            )
        kubernetes_config = None
        if settings.kubernetes:
            kubernetes_config = KubernetesConfig(
                service_secrets=tuple(settings.kubernetes.service_secrets))
        log_level = os.getenv("SAFIR_LOG_LEVEL", settings.loglevel)
        config = cls(
            realm=settings.realm,
            session_secret=session_secret.decode(),
            redis_url=settings.redis_url,
            redis_password=redis_password,
            bootstrap_token=bootstrap_token,
            proxies=tuple(settings.proxies if settings.proxies else []),
            after_logout_url=str(settings.after_logout_url),
            issuer=issuer_config,
            verifier=verifier_config,
            github=github_config,
            oidc=oidc_config,
            oidc_server=oidc_server_config,
            known_scopes=settings.known_scopes or {},
            database_url=database_url,
            initial_admins=tuple(settings.initial_admins),
            token_lifetime=timedelta(minutes=settings.issuer.exp_minutes),
            kubernetes=kubernetes_config,
            safir=SafirConfig(log_level=log_level),
        )

        # Configure logging.
        configure_logging(
            profile=config.safir.profile,
            log_level=config.safir.log_level,
            name=config.safir.logger_name,
        )

        # Return the completed configuration.
        return config
Ejemplo n.º 16
0
async def test_create_delete_modify(setup: SetupTest,
                                    caplog: LogCaptureFixture) -> None:
    user_info = TokenUserInfo(
        username="******",
        name="Example Person",
        email="*****@*****.**",
        uid=45613,
        groups=[TokenGroup(name="foo", id=12313)],
    )
    token_service = setup.factory.create_token_service()
    session_token = await token_service.create_session_token(
        user_info,
        scopes=["read:all", "exec:admin", "user:token"],
        ip_address="127.0.0.1",
    )
    csrf = await setup.login(session_token)

    expires = current_datetime() + timedelta(days=100)
    r = await setup.client.post(
        "/auth/api/v1/users/example/tokens",
        headers={"X-CSRF-Token": csrf},
        json={
            "token_name": "some token",
            "scopes": ["read:all"],
            "expires": int(expires.timestamp()),
        },
    )
    assert r.status_code == 201
    assert r.json() == {"token": ANY}
    user_token = Token.from_str(r.json()["token"])
    token_url = r.headers["Location"]
    assert token_url == f"/auth/api/v1/users/example/tokens/{user_token.key}"

    r = await setup.client.get(token_url)
    assert r.status_code == 200
    info = r.json()
    assert info == {
        "token": user_token.key,
        "username": "******",
        "token_name": "some token",
        "token_type": "user",
        "scopes": ["read:all"],
        "created": ANY,
        "expires": int(expires.timestamp()),
    }

    # Check that this is the same information as is returned by the token-info
    # route.  This is a bit tricky to do since the cookie will take precedence
    # over the Authorization header, but we can't just delete the cookie since
    # we'll lose the CSRF token.  Save the cookie and delete it, and then
    # later restore it.
    cookie = setup.client.cookies.pop(COOKIE_NAME)
    r = await setup.client.get(
        "/auth/api/v1/token-info",
        headers={"Authorization": f"bearer {user_token}"},
    )
    assert r.status_code == 200
    assert r.json() == info
    setup.client.cookies.set(COOKIE_NAME, cookie, domain=TEST_HOSTNAME)

    # Listing all tokens for this user should return the user token and a
    # session token.
    r = await setup.client.get("/auth/api/v1/users/example/tokens")
    assert r.status_code == 200
    assert r.json() == sorted(
        [
            {
                "token": session_token.key,
                "username": "******",
                "token_type": "session",
                "scopes": ["exec:admin", "read:all", "user:token"],
                "created": ANY,
                "expires": ANY,
            },
            info,
        ],
        key=lambda t: t["token"],
    )

    # Change the name, scope, and expiration of the token.
    caplog.clear()
    new_expires = current_datetime() + timedelta(days=200)
    r = await setup.client.patch(
        token_url,
        headers={"X-CSRF-Token": csrf},
        json={
            "token_name": "happy token",
            "scopes": ["exec:admin"],
            "expires": int(new_expires.timestamp()),
        },
    )
    assert r.status_code == 201
    assert r.json() == {
        "token": user_token.key,
        "username": "******",
        "token_name": "happy token",
        "token_type": "user",
        "scopes": ["exec:admin"],
        "created": ANY,
        "expires": int(new_expires.timestamp()),
    }

    # Check the logging.  Regression test for a bug where new expirations
    # would be logged as raw datetime objects instead of timestamps.
    log = json.loads(caplog.record_tuples[0][2])
    assert log == {
        "expires": int(new_expires.timestamp()),
        "event": "Modified token",
        "key": user_token.key,
        "level": "info",
        "logger": "gafaelfawr",
        "method": "PATCH",
        "path": token_url,
        "remote": "127.0.0.1",
        "request_id": ANY,
        "scope": "exec:admin read:all user:token",
        "token": session_token.key,
        "token_name": "happy token",
        "token_scope": "exec:admin",
        "token_source": "cookie",
        "user": "******",
        "user_agent": ANY,
    }

    # Delete the token.
    r = await setup.client.delete(token_url, headers={"X-CSRF-Token": csrf})
    assert r.status_code == 204
    r = await setup.client.get(token_url)
    assert r.status_code == 404

    # Deleting again should return 404.
    r = await setup.client.delete(token_url, headers={"X-CSRF-Token": csrf})
    assert r.status_code == 404

    # This user should now have only one token.
    r = await setup.client.get("/auth/api/v1/users/example/tokens")
    assert r.status_code == 200
    assert len(r.json()) == 1

    # We should be able to see the change history for the token.
    r = await setup.client.get(token_url + "/change-history")
    assert r.status_code == 200
    assert r.json() == [
        {
            "token": user_token.key,
            "username": "******",
            "token_type": "user",
            "token_name": "happy token",
            "scopes": ["exec:admin"],
            "expires": int(new_expires.timestamp()),
            "actor": "example",
            "action": "revoke",
            "ip_address": "127.0.0.1",
            "event_time": ANY,
        },
        {
            "token": user_token.key,
            "username": "******",
            "token_type": "user",
            "token_name": "happy token",
            "scopes": ["exec:admin"],
            "expires": int(new_expires.timestamp()),
            "actor": "example",
            "action": "edit",
            "old_token_name": "some token",
            "old_scopes": ["read:all"],
            "old_expires": int(expires.timestamp()),
            "ip_address": "127.0.0.1",
            "event_time": ANY,
        },
        {
            "token": user_token.key,
            "username": "******",
            "token_type": "user",
            "token_name": "some token",
            "scopes": ["read:all"],
            "expires": int(expires.timestamp()),
            "actor": "example",
            "action": "create",
            "ip_address": "127.0.0.1",
            "event_time": ANY,
        },
    ]
Ejemplo n.º 17
0
async def test_create_admin(setup: SetupTest) -> None:
    """Test creating a token through the admin interface."""
    token_data = await setup.create_session_token(scopes=["exec:admin"])
    csrf = await setup.login(token_data.token)

    r = await setup.client.post(
        "/auth/api/v1/tokens",
        headers={"X-CSRF-Token": csrf},
        json={
            "username": "******",
            "token_type": "service"
        },
    )
    assert r.status_code == 403

    token_data = await setup.create_session_token(scopes=["admin:token"])
    csrf = await setup.login(token_data.token)

    now = datetime.now(tz=timezone.utc)
    expires = int((now + timedelta(days=2)).timestamp())
    r = await setup.client.post(
        "/auth/api/v1/tokens",
        headers={"X-CSRF-Token": csrf},
        json={
            "username": "******",
            "token_type": "service",
            "scopes": ["admin:token"],
            "expires": expires,
            "name": "A Service",
            "uid": 1234,
            "email": "*****@*****.**",
            "groups": [{
                "name": "some-group",
                "id": 12381
            }],
        },
    )
    assert r.status_code == 201
    assert r.json() == {"token": ANY}
    service_token = Token.from_str(r.json()["token"])
    token_url = f"/auth/api/v1/users/a-service/tokens/{service_token.key}"
    assert r.headers["Location"] == token_url

    setup.logout()
    r = await setup.client.get(
        "/auth/api/v1/token-info",
        headers={"Authorization": f"bearer {str(service_token)}"},
    )
    assert r.status_code == 200
    assert r.json() == {
        "token": service_token.key,
        "username": "******",
        "token_type": "service",
        "scopes": ["admin:token"],
        "created": ANY,
        "expires": expires,
    }
    r = await setup.client.get(
        "/auth/api/v1/user-info",
        headers={"Authorization": f"bearer {str(service_token)}"},
    )
    assert r.status_code == 200
    assert r.json() == {
        "username": "******",
        "name": "A Service",
        "email": "*****@*****.**",
        "uid": 1234,
        "email": "*****@*****.**",
        "groups": [{
            "name": "some-group",
            "id": 12381
        }],
    }

    r = await setup.client.post(
        "/auth/api/v1/tokens",
        headers={"Authorization": f"bearer {str(service_token)}"},
        json={
            "username": "******",
            "token_type": "session"
        },
    )
    assert r.status_code == 422
    r = await setup.client.post(
        "/auth/api/v1/tokens",
        headers={"Authorization": f"bearer {str(service_token)}"},
        json={
            "username": "******",
            "token_type": "user"
        },
    )
    assert r.status_code == 422
    r = await setup.client.post(
        "/auth/api/v1/tokens",
        headers={"Authorization": f"bearer {str(service_token)}"},
        json={
            "username": "******",
            "token_type": "user",
            "token_name": "some token",
            "expires": int(datetime.now(tz=timezone.utc).timestamp()),
        },
    )
    assert r.status_code == 422
    assert r.json()["detail"][0]["type"] == "invalid_expires"
    r = await setup.client.post(
        "/auth/api/v1/tokens",
        headers={"Authorization": f"bearer {str(service_token)}"},
        json={
            "username": "******",
            "token_type": "user",
            "token_name": "some token",
            "scopes": ["bogus:scope"],
        },
    )
    assert r.status_code == 422
    assert r.json()["detail"][0]["type"] == "invalid_scopes"

    r = await setup.client.post(
        "/auth/api/v1/tokens",
        headers={"Authorization": f"bearer {str(service_token)}"},
        json={
            "username": "******",
            "token_type": "user",
            "token_name": "some token",
        },
    )
    assert r.status_code == 201
    assert r.json() == {"token": ANY}
    user_token = Token.from_str(r.json()["token"])
    token_url = f"/auth/api/v1/users/a-user/tokens/{user_token.key}"
    assert r.headers["Location"] == token_url

    # Successfully create a user token.
    r = await setup.client.get(
        "/auth/api/v1/token-info",
        headers={"Authorization": f"bearer {str(user_token)}"},
    )
    assert r.status_code == 200
    assert r.json() == {
        "token": user_token.key,
        "username": "******",
        "token_type": "user",
        "token_name": "some token",
        "scopes": [],
        "created": ANY,
    }
    r = await setup.client.get(
        "/auth/api/v1/user-info",
        headers={"Authorization": f"bearer {str(user_token)}"},
    )
    assert r.status_code == 200
    assert r.json() == {"username": "******"}

    # Check handling of duplicate token name errors.
    r = await setup.client.post(
        "/auth/api/v1/tokens",
        headers={"Authorization": f"bearer {str(service_token)}"},
        json={
            "username": "******",
            "token_type": "user",
            "token_name": "some token",
        },
    )
    assert r.status_code == 422
    assert r.json()["detail"][0]["type"] == "duplicate_token_name"

    # Check handling of an invalid username.
    r = await setup.client.post(
        "/auth/api/v1/tokens",
        headers={"Authorization": f"bearer {str(service_token)}"},
        json={
            "username": "******",
            "token_type": "user",
            "token_name": "some token",
        },
    )
    assert r.status_code == 422

    # Check that the bootstrap token also works.
    r = await setup.client.post(
        "/auth/api/v1/tokens",
        headers={
            "Authorization": f"bearer {str(setup.config.bootstrap_token)}"
        },
        json={
            "username": "******",
            "token_type": "service"
        },
    )
    assert r.status_code == 201
Ejemplo n.º 18
0
    async def authenticate(self,
                           context: RequestContext,
                           x_csrf_token: Optional[str] = None) -> TokenData:
        """Authenticate the request.

        Always check the user's cookie-based session first before checking the
        ``Authorization`` header because some applications (JupyterHub, for
        instance) may use the ``Authorization`` header for their own purposes.

        If the request was authenticated via a browser cookie rather than a
        provided ``Authorization`` header, and the method was something other
        than ``GET`` or ``OPTIONS``, require and verify the CSRF header as
        well.

        Parameters
        ----------
        context : `gafaelfawr.dependencies.context.RequestContext`
            The request context.
        x_csrf_token : `str`, optional
            The value of the ``X-CSRF-Token`` header, if provided.

        Returns
        -------
        data : `gafaelfawr.models.token.TokenData`
            The data associated with the verified token.

        Raises
        ------
        fastapi.HTTPException
            If authentication is not provided or is not valid.
        """
        token = context.state.token
        if token:
            context.rebind_logger(token_source="cookie")
            self._verify_csrf(context, x_csrf_token)
        elif not self.require_session:
            try:
                token_str = parse_authorization(context)
                if token_str:
                    token = Token.from_str(token_str)
            except (InvalidRequestError, InvalidTokenError) as e:
                raise generate_challenge(context, self.auth_type, e)
        if not token:
            raise self._redirect_or_error(context)

        if self.allow_bootstrap_token:
            if token == context.config.bootstrap_token:
                bootstrap_data = TokenData.bootstrap_token()
                context.rebind_logger(
                    token="<bootstrap>",
                    user="******",
                    scope=" ".join(sorted(bootstrap_data.scopes)),
                )
                context.logger.info("Authenticated with bootstrap token")
                return bootstrap_data

        token_service = context.factory.create_token_service()
        data = await token_service.get_data(token)
        if not data:
            if context.state.token:
                raise self._redirect_or_error(context)
            else:
                exc = InvalidTokenError("Token is not valid")
                raise generate_challenge(context, self.auth_type, exc)

        context.rebind_logger(
            token=token.key,
            user=data.username,
            scope=" ".join(sorted(data.scopes)),
        )

        if self.require_scope and self.require_scope not in data.scopes:
            msg = f"Token does not have required scope {self.require_scope}"
            context.logger.info("Permission denied", error=msg)
            raise PermissionDeniedError(msg)

        return data