Ejemplo n.º 1
0
async def test_expiration(setup: SetupTest) -> None:
    """The cache is valid until half the lifetime of the child token."""
    token_data = await setup.create_session_token(scopes=["read:all"])
    lifetime = setup.config.token_lifetime
    now = current_datetime()
    storage = RedisStorage(TokenData, setup.config.session_secret, setup.redis)
    token_store = TokenRedisStore(storage, setup.logger)
    token_cache = setup.factory.create_token_cache()

    # Store a token whose expiration is five seconds more than half the
    # typical token lifetime in the future and cache that token as an internal
    # token for our session token.
    created = now - timedelta(seconds=lifetime.total_seconds() // 2)
    expires = created + setup.config.token_lifetime + timedelta(seconds=5)
    internal_token_data = TokenData(
        token=Token(),
        username=token_data.username,
        token_type=TokenType.internal,
        scopes=["read:all"],
        created=created,
        expires=expires,
    )
    await token_store.store_data(internal_token_data)
    token_cache.store_internal_token(
        internal_token_data.token, token_data, "some-service", ["read:all"]
    )

    # The cache should return this token.
    assert internal_token_data.token == await token_cache.get_internal_token(
        token_data, "some-service", ["read:all"]
    )

    # Now change the expiration to be ten seconds earlier, which should make
    # the remaining lifetime less than half the total lifetime, and replace
    # replace the stored token with that new version.
    internal_token_data.expires = expires - timedelta(seconds=20)
    await token_store.store_data(internal_token_data)

    # The cache should now decline to return the token.
    assert not await token_cache.get_internal_token(
        token_data, "some-service", ["read:all"]
    )

    # Do the same test with a notebook token.
    notebook_token_data = TokenData(
        token=Token(),
        username=token_data.username,
        token_type=TokenType.notebook,
        scopes=["read:all"],
        created=created,
        expires=expires,
    )
    await token_store.store_data(notebook_token_data)
    token_cache.store_notebook_token(notebook_token_data.token, token_data)
    token = notebook_token_data.token
    assert token == await token_cache.get_notebook_token(token_data)
    notebook_token_data.expires = expires - timedelta(seconds=20)
    await token_store.store_data(notebook_token_data)
    assert not await token_cache.get_notebook_token(token_data)
Ejemplo n.º 2
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.º 3
0
async def test_invalid(config: Config, factory: ComponentFactory) -> None:
    redis = await redis_dependency()
    token_service = factory.create_token_service()
    expires = int(timedelta(days=1).total_seconds())

    # No such key.
    token = Token()
    assert await token_service.get_data(token) is None

    # Invalid encrypted blob.
    await redis.set(f"token:{token.key}", "foo", ex=expires)
    assert await token_service.get_data(token) is None

    # Malformed session.
    fernet = Fernet(config.session_secret.encode())
    raw_data = fernet.encrypt(b"malformed json")
    await redis.set(f"token:{token.key}", raw_data, ex=expires)
    assert await token_service.get_data(token) is None

    # Mismatched token.
    data = TokenData(
        token=Token(),
        username="******",
        token_type=TokenType.session,
        scopes=[],
        created=int(current_datetime().timestamp()),
        name="Some User",
        uid=12345,
    )
    session = fernet.encrypt(data.json().encode())
    await redis.set(f"token:{token.key}", session, ex=expires)
    assert await token_service.get_data(token) is None

    # Missing required fields.
    json_data = {
        "token": {
            "key": token.key,
            "secret": token.secret,
        },
        "token_type": "session",
        "scopes": [],
        "created": int(current_datetime().timestamp()),
        "name": "Some User",
    }
    raw_data = fernet.encrypt(json.dumps(json_data).encode())
    await redis.set(f"token:{token.key}", raw_data, ex=expires)
    assert await token_service.get_data(token) is None

    # Fix the session store and confirm we can retrieve the manually-stored
    # session.
    json_data["username"] = "******"
    raw_data = fernet.encrypt(json.dumps(json_data).encode())
    await redis.set(f"token:{token.key}", raw_data, ex=expires)
    new_data = await token_service.get_data(token)
    assert new_data == TokenData.parse_obj(json_data)
Ejemplo n.º 4
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.º 5
0
async def test_invalid(factory: ComponentFactory) -> None:
    """Invalid tokens should not be returned even if cached."""
    token_data = await create_session_token(factory, scopes=["read:all"])
    token_cache = factory.create_token_cache_service()
    internal_token = Token()
    notebook_token = Token()

    token_cache.store_internal_token(internal_token, token_data,
                                     "some-service", ["read:all"])
    token_cache.store_notebook_token(notebook_token, token_data)

    assert internal_token != await token_cache.get_internal_token(
        token_data, "some-service", ["read:all"], "127.0.0.1")
    assert notebook_token != await token_cache.get_notebook_token(
        token_data, "127.0.0.1")
Ejemplo n.º 6
0
async def test_invalid(setup: SetupTest) -> None:
    """Invalid tokens should not be returned even if cached."""
    token_data = await setup.create_session_token(scopes=["read:all"])
    token_cache = setup.factory.create_token_cache()
    internal_token = Token()
    notebook_token = Token()

    token_cache.store_internal_token(
        internal_token, token_data, "some-service", ["read:all"]
    )
    token_cache.store_notebook_token(notebook_token, token_data)

    assert not await token_cache.get_internal_token(
        token_data, "some-service", ["read:all"]
    )
    assert not await token_cache.get_notebook_token(token_data)
Ejemplo n.º 7
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.º 8
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.º 9
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.º 10
0
async def add_expired_session_token(
    user_info: TokenUserInfo,
    *,
    scopes: List[str],
    ip_address: str,
    session: AsyncSession,
) -> None:
    """Add an expired session token to the database.

    This requires going beneath the service layer, since the service layer
    rejects creation of expired tokens (since apart from testing this isn't a
    sensible thing to want to do).

    This does not add the token to Redis, since Redis will refuse to add it
    with a negative expiration time, so can only be used for tests that
    exclusively use the database.

    Parameters
    ----------
    user_info : `gafaelfawr.models.token.TokenUserInfo`
        The user information to associate with the token.
    scopes : List[`str`]
        The scopes of the token.
    ip_address : `str`
        The IP address from which the request came.
    session : `sqlalchemy.ext.asyncio.AsyncSession`
        The database session.
    """
    token_db_store = TokenDatabaseStore(session)
    token_change_store = TokenChangeHistoryStore(session)

    token = Token()
    created = current_datetime()
    expires = created - timedelta(minutes=10)
    data = TokenData(
        token=token,
        token_type=TokenType.session,
        scopes=scopes,
        created=created,
        expires=expires,
        **user_info.dict(),
    )
    history_entry = TokenChangeHistoryEntry(
        token=token.key,
        username=data.username,
        token_type=TokenType.session,
        scopes=scopes,
        expires=expires,
        actor=data.username,
        action=TokenChange.create,
        ip_address=ip_address,
        event_time=created,
    )

    await token_db_store.add(data)
    await token_change_store.add(history_entry)
Ejemplo n.º 11
0
    async def create_session_token(self, user_info: TokenUserInfo, *,
                                   scopes: List[str],
                                   ip_address: str) -> Token:
        """Create a new session token.

        Parameters
        ----------
        user_info : `gafaelfawr.models.token.TokenUserInfo`
            The user information to associate with the token.
        scopes : List[`str`]
            The scopes of the token.
        ip_address : `str`
            The IP address from which the request came.

        Returns
        -------
        token : `gafaelfawr.models.token.Token`
            The newly-created token.

        Raises
        ------
        gafaelfawr.exceptions.PermissionDeniedError
            If the provided username is invalid.
        """
        self._validate_username(user_info.username)
        scopes = sorted(scopes)

        token = Token()
        created = current_datetime()
        expires = created + self._config.token_lifetime
        data = TokenData(
            token=token,
            token_type=TokenType.session,
            scopes=scopes,
            created=created,
            expires=expires,
            **user_info.dict(),
        )
        history_entry = TokenChangeHistoryEntry(
            token=token.key,
            username=data.username,
            token_type=TokenType.session,
            scopes=scopes,
            expires=expires,
            actor=data.username,
            action=TokenChange.create,
            ip_address=ip_address,
            event_time=created,
        )

        await self._token_redis_store.store_data(data)
        with self._transaction_manager.transaction():
            self._token_db_store.add(data)
            self._token_change_store.add(history_entry)

        return token
Ejemplo n.º 12
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.º 13
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.º 14
0
async def test_invalid_auth(setup: SetupTest) -> None:
    r = await setup.client.get(
        "/auth",
        params={"scope": "exec:admin"},
        headers={"Authorization": "Bearer"},
    )
    assert r.status_code == 400
    authenticate = parse_www_authenticate(r.headers["WWW-Authenticate"])
    assert isinstance(authenticate, AuthErrorChallenge)
    assert authenticate.auth_type == AuthType.Bearer
    assert authenticate.realm == setup.config.realm
    assert authenticate.error == AuthError.invalid_request

    r = await setup.client.get(
        "/auth",
        params={"scope": "exec:admin"},
        headers={"Authorization": "token foo"},
    )
    assert r.status_code == 400
    authenticate = parse_www_authenticate(r.headers["WWW-Authenticate"])
    assert isinstance(authenticate, AuthErrorChallenge)
    assert authenticate.auth_type == AuthType.Bearer
    assert authenticate.realm == setup.config.realm
    assert authenticate.error == AuthError.invalid_request

    r = await setup.client.get(
        "/auth",
        params={"scope": "exec:admin"},
        headers={"Authorization": "Bearer token"},
    )
    assert r.status_code == 401
    assert r.headers["Cache-Control"] == "no-cache, must-revalidate"
    authenticate = parse_www_authenticate(r.headers["WWW-Authenticate"])
    assert isinstance(authenticate, AuthErrorChallenge)
    assert authenticate.auth_type == AuthType.Bearer
    assert authenticate.realm == setup.config.realm
    assert authenticate.error == AuthError.invalid_token

    # Create a nonexistent token.
    token = Token()
    r = await setup.client.get(
        "/auth",
        params={"scope": "exec:admin"},
        headers={"Authorization": f"Bearer {token}"},
    )
    assert r.status_code == 401
    assert r.headers["Cache-Control"] == "no-cache, must-revalidate"
    authenticate = parse_www_authenticate(r.headers["WWW-Authenticate"])
    assert isinstance(authenticate, AuthErrorChallenge)
    assert authenticate.auth_type == AuthType.Bearer
    assert authenticate.realm == setup.config.realm
    assert authenticate.error == AuthError.invalid_token
Ejemplo n.º 15
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.º 16
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.º 17
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.º 18
0
def parse_settings(path: Path, fix_token: bool = False) -> None:
    """Parse the settings file and see if any exceptions are thrown.

    Parameters
    ----------
    path : `pathlib.Path`
        The path to the settings file to test.
    fix_token : `bool`, optional
        Whether to fix an invalid ``bootstrap_token`` before checking the
        settings file.  Some examples have intentionally invalid tokens.
    """
    with path.open("r") as f:
        settings = yaml.safe_load(f)

    # Avoid errors from an invalid bootstrap token in one of the examples.
    if fix_token and "bootstrap_token" in settings:
        settings["bootstrap_token"] = str(Token())

    Settings.parse_obj(settings)
Ejemplo n.º 19
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.º 20
0
async def test_login_no_auth(client: AsyncClient, config: Config,
                             factory: ComponentFactory) -> None:
    r = await client.get("/auth/api/v1/login")
    assert_unauthorized_is_correct(r, config)

    # An Authorization header with a valid token still redirects.
    token_data = await create_session_token(factory)
    r = await client.get(
        "/auth/api/v1/login",
        headers={"Authorization": f"bearer {token_data.token}"},
    )
    assert_unauthorized_is_correct(r, config)

    # A token with no underlying Redis representation is ignored.
    state = State(token=Token())
    r = await client.get(
        "/auth/api/v1/login",
        cookies={COOKIE_NAME: await state.as_cookie()},
    )
    assert_unauthorized_is_correct(r, config)

    # Likewise with a cookie containing a malformed token.  This requires a
    # bit more work to assemble.
    key = config.session_secret.encode()
    fernet = Fernet(key)
    data = {"token": "bad-token"}
    bad_cookie = fernet.encrypt(json.dumps(data).encode()).decode()
    r = await client.get(
        "/auth/api/v1/login",
        cookies={COOKIE_NAME: bad_cookie},
    )
    assert_unauthorized_is_correct(r, config)

    # And finally check with a mangled state that won't decrypt.
    bad_cookie = "XXX" + await state.as_cookie()
    r = await client.get(
        "/auth/api/v1/login",
        cookies={COOKIE_NAME: bad_cookie},
    )
    assert_unauthorized_is_correct(r, config)
Ejemplo n.º 21
0
async def test_create_not_ours(setup: SetupTest,
                               mock_kubernetes: MockCoreV1Api,
                               caplog: LogCaptureFixture) -> None:
    assert setup.config.kubernetes
    assert len(setup.config.kubernetes.service_secrets) >= 1
    service_secret = setup.config.kubernetes.service_secrets[-1]
    kubernetes_service = setup.factory.create_kubernetes_service()

    # Create a secret that should exist but doesn't have our annotation.
    secret = V1Secret(
        api_version="v1",
        data={"token": token_as_base64(Token())},
        metadata=V1ObjectMeta(
            name=service_secret.secret_name,
            namespace=service_secret.secret_namespace,
        ),
        type="Opaque",
    )
    mock_kubernetes.create_namespaced_secret(service_secret.secret_namespace,
                                             secret)

    # Now run the synchronization.  secret_one and secret_two should be left
    # unchanged, and we should log errors about failing to do the update.
    await kubernetes_service.update_service_secrets()
    objects = mock_kubernetes.get_all_objects_for_test()
    assert secret in objects
    assert json.loads(caplog.record_tuples[-1][2]) == {
        "event": (f"Creating {service_secret.secret_namespace}"
                  f"/{service_secret.secret_name} failed"),
        "error": (f"Kubernetes API error: (500)\n"
                  f"Reason: {service_secret.secret_namespace}"
                  f"/{service_secret.secret_name} exists\n"),
        "level":
        "error",
        "logger":
        "gafaelfawr",
    }
Ejemplo n.º 22
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.º 23
0
async def test_ignore(setup: SetupTest,
                      mock_kubernetes: MockCoreV1Api) -> None:
    assert setup.config.kubernetes
    kubernetes_service = setup.factory.create_kubernetes_service()

    # Create a secret without the expected label.
    secret_one = V1Secret(
        api_version="v1",
        data={"foo": "bar"},
        metadata=V1ObjectMeta(name="secret-one", namespace="mobu"),
        type="Opaque",
    )
    mock_kubernetes.create_namespaced_secret("mobu", secret_one)

    # Create a secret with the expected label but a different value.
    secret_two = V1Secret(
        api_version="v1",
        data={"token": token_as_base64(Token())},
        metadata=V1ObjectMeta(
            labels={KUBERNETES_TOKEN_TYPE_LABEL: "other"},
            name="secret-two",
            namespace="elsewhere",
        ),
        type="Opaque",
    )
    mock_kubernetes.create_namespaced_secret("elsewhere", secret_two)

    # Update the secrets.  Both of our secrets should survive unmolested.
    await kubernetes_service.update_service_secrets()
    objects = mock_kubernetes.get_all_objects_for_test()
    assert secret_one in objects
    assert secret_two in objects

    # Delete our secrets and then check that the created secrets are right.
    mock_kubernetes.delete_namespaced_secret("secret-one", "mobu")
    mock_kubernetes.delete_namespaced_secret("secret-two", "elsewhere")
    await assert_kubernetes_secrets_match_config(setup, mock_kubernetes)
Ejemplo n.º 24
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.º 25
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.º 26
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.º 27
0
async def test_expiration(config: Config, factory: ComponentFactory) -> None:
    """The cache is valid until half the lifetime of the child token."""
    token_data = await create_session_token(factory, scopes=["read:all"])
    lifetime = config.token_lifetime
    now = current_datetime()
    redis = await redis_dependency()
    logger = structlog.get_logger(config.safir.logger_name)
    storage = RedisStorage(TokenData, config.session_secret, redis)
    token_store = TokenRedisStore(storage, logger)
    token_cache = factory.create_token_cache_service()

    # Store a token whose expiration is five seconds more than half the
    # typical token lifetime in the future and cache that token as an internal
    # token for our session token.
    created = now - timedelta(seconds=lifetime.total_seconds() // 2)
    expires = created + lifetime + timedelta(seconds=5)
    internal_token_data = TokenData(
        token=Token(),
        username=token_data.username,
        token_type=TokenType.internal,
        scopes=["read:all"],
        created=created,
        expires=expires,
    )
    await token_store.store_data(internal_token_data)
    token_cache.store_internal_token(internal_token_data.token, token_data,
                                     "some-service", ["read:all"])

    # The cache should return this token.
    assert internal_token_data.token == await token_cache.get_internal_token(
        token_data, "some-service", ["read:all"], "127.0.0.1")

    # Now change the expiration to be ten seconds earlier, which should make
    # the remaining lifetime less than half the total lifetime, and replace
    # replace the stored token with that new version.
    internal_token_data.expires = expires - timedelta(seconds=20)
    await token_store.store_data(internal_token_data)

    # The cache should now decline to return the token and generate a new one.
    old_token = internal_token_data.token
    async with factory.session.begin():
        assert old_token != await token_cache.get_internal_token(
            token_data, "some-service", ["read:all"], "127.0.0.1")

    # Do the same test with a notebook token.
    notebook_token_data = TokenData(
        token=Token(),
        username=token_data.username,
        token_type=TokenType.notebook,
        scopes=["read:all"],
        created=created,
        expires=expires,
    )
    await token_store.store_data(notebook_token_data)
    token_cache.store_notebook_token(notebook_token_data.token, token_data)
    assert notebook_token_data.token == await token_cache.get_notebook_token(
        token_data, "127.0.0.1")
    notebook_token_data.expires = expires - timedelta(seconds=20)
    await token_store.store_data(notebook_token_data)
    old_token = notebook_token_data.token
    async with factory.session.begin():
        assert old_token != await token_cache.get_notebook_token(
            token_data, "127.0.0.1")
Ejemplo n.º 28
0
    async def get_notebook_token(self, token_data: TokenData,
                                 ip_address: str) -> Token:
        """Get or create a new notebook token.

        The new token will have the same expiration time as the existing token
        on which it's based unless that expiration time is longer than the
        expiration time of normal interactive tokens, in which case it will be
        capped at the interactive token expiration time.

        Parameters
        ----------
        token_data : `gafaelfawr.models.token.TokenData`
            The authentication data on which to base the new token.
        ip_address : `str`
            The IP address from which the request came.

        Returns
        -------
        token : `gafaelfawr.models.token.Token`
            The newly-created token.

        Raises
        ------
        gafaelfawr.exceptions.PermissionDeniedError
            If the username is invalid.
        """
        self._validate_username(token_data.username)

        # See if there is a cached token.
        token = await self._token_cache.get_notebook_token(token_data)
        if token:
            return token

        # See if there's already a matching notebook token.
        key = self._token_db_store.get_notebook_token_key(
            token_data, self._minimum_expiration(token_data))
        if key:
            data = await self._token_redis_store.get_data_by_key(key)
            if data:
                self._token_cache.store_notebook_token(data.token, token_data)
                return data.token

        # There is not, so we need to create a new one.
        token = Token()
        created = current_datetime()
        expires = created + self._config.token_lifetime
        if token_data.expires and token_data.expires < expires:
            expires = token_data.expires
        data = TokenData(
            token=token,
            username=token_data.username,
            token_type=TokenType.notebook,
            scopes=token_data.scopes,
            created=created,
            expires=expires,
            name=token_data.name,
            email=token_data.email,
            uid=token_data.uid,
            groups=token_data.groups,
        )
        history_entry = TokenChangeHistoryEntry(
            token=token.key,
            username=data.username,
            token_type=TokenType.notebook,
            parent=token_data.token.key,
            scopes=data.scopes,
            expires=expires,
            actor=token_data.username,
            action=TokenChange.create,
            ip_address=ip_address,
            event_time=created,
        )

        await self._token_redis_store.store_data(data)
        with self._transaction_manager.transaction():
            self._token_db_store.add(data, parent=token_data.token.key)
            self._token_change_store.add(history_entry)

        # Cache the token and return it.
        self._logger.info("Created new notebook token", key=token.key)
        self._token_cache.store_notebook_token(token, token_data)
        return token
Ejemplo n.º 29
0
    async def create_token_from_admin_request(
        self,
        request: AdminTokenRequest,
        auth_data: TokenData,
        *,
        ip_address: Optional[str],
    ) -> Token:
        """Create a new service or user token from an admin request.

        Parameters
        ----------
        request : `gafaelfawr.models.token.AdminTokenRequest`
            The incoming request.
        auth_data : `gafaelfawr.models.token.TokenData`
            The data for the authenticated user making the request.
        ip_address : `str` or `None`
            The IP address from which the request came, or `None` for internal
            requests by Gafaelfawr.

        Returns
        -------
        token : `gafaelfawr.models.token.Token`
            The newly-created token.

        Raises
        ------
        gafaelfawr.exceptions.PermissionDeniedError
            If the provided username is invalid.
        """
        self._check_authorization(request.username,
                                  auth_data,
                                  require_admin=True)
        self._validate_username(request.username)
        self._validate_scopes(request.scopes)
        self._validate_expires(request.expires)

        token = Token()
        created = current_datetime()
        data = TokenData(
            token=token,
            username=request.username,
            token_type=request.token_type,
            scopes=request.scopes,
            created=created,
            expires=request.expires,
            name=request.name,
            email=request.email,
            uid=request.uid,
            groups=request.groups,
        )
        history_entry = TokenChangeHistoryEntry(
            token=token.key,
            username=data.username,
            token_type=data.token_type,
            token_name=request.token_name,
            scopes=data.scopes,
            expires=request.expires,
            actor=auth_data.username,
            action=TokenChange.create,
            ip_address=ip_address,
            event_time=created,
        )

        await self._token_redis_store.store_data(data)
        with self._transaction_manager.transaction():
            self._token_db_store.add(data, token_name=request.token_name)
            self._token_change_store.add(history_entry)

        if data.token_type == TokenType.user:
            self._logger.info(
                "Created new user token",
                key=token.key,
                token_name=request.token_name,
                token_scope=",".join(data.scopes),
                token_username=data.username,
            )
        else:
            self._logger.info(
                "Created new service token",
                key=token.key,
                token_scope=",".join(data.scopes),
                token_username=data.username,
            )
        return token
Ejemplo n.º 30
0
    async def create_user_token(
        self,
        auth_data: TokenData,
        username: str,
        *,
        token_name: str,
        scopes: List[str],
        expires: Optional[datetime] = None,
        ip_address: str,
    ) -> Token:
        """Add a new user token.

        Parameters
        ----------
        auth_data : `gafaelfawr.models.token.TokenData`
            The token data for the authentication token of the user creating
            a user token.
        username : `str`
            The username for which to create a token.
        token_name : `str`
            The name of the token.
        scopes : List[`str`]
            The scopes of the token.
        expires : `datetime` or `None`
            When the token should expire.  If not given, defaults to the
            expiration of the authentication token taken from ``data``.
        ip_address : `str`
            The IP address from which the request came.

        Returns
        -------
        token : `gafaelfawr.models.token.Token`
            The newly-created token.

        Raises
        ------
        gafaelfawr.exceptions.DuplicateTokenNameError
            A token with this name for this user already exists.
        gafaelfawr.exceptions.InvalidExpiresError
            The provided expiration time was invalid.
        gafaelfawr.exceptions.PermissionDeniedError
            If the given username didn't match the user information in the
            authentication token, or if the specified username is invalid.

        Notes
        -----
        This can only be used by the user themselves, not by a token
        administrator, because this API does not provide a way to set the
        additional user information for the token.  Once the user information
        no longer needs to be tracked by the token system, it can be unified
        with ``create_token_from_admin_request``.
        """
        self._check_authorization(username, auth_data, require_same_user=True)
        self._validate_username(username)
        self._validate_expires(expires)
        self._validate_scopes(scopes, auth_data)
        scopes = sorted(scopes)

        token = Token()
        created = current_datetime()
        data = TokenData(
            token=token,
            username=username,
            token_type=TokenType.user,
            scopes=scopes,
            created=created,
            expires=expires,
            name=auth_data.name,
            email=auth_data.email,
            uid=auth_data.uid,
            groups=auth_data.groups,
        )
        history_entry = TokenChangeHistoryEntry(
            token=token.key,
            username=data.username,
            token_type=TokenType.user,
            token_name=token_name,
            scopes=scopes,
            expires=expires,
            actor=auth_data.username,
            action=TokenChange.create,
            ip_address=ip_address,
            event_time=created,
        )

        await self._token_redis_store.store_data(data)
        with self._transaction_manager.transaction():
            self._token_db_store.add(data, token_name=token_name)
            self._token_change_store.add(history_entry)

        self._logger.info(
            "Created new user token",
            key=token.key,
            token_name=token_name,
            token_scope=",".join(data.scopes),
        )

        return token