예제 #1
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)
예제 #2
0
async def assert_kubernetes_secrets_are_correct(
    factory: ComponentFactory, mock: MockKubernetesApi, is_fresh: bool = True
) -> None:
    token_service = factory.create_token_service()

    # Get all of the GafaelfawrServiceToken custom objects.
    service_tokens = mock.get_all_objects_for_test("GafaelfawrServiceToken")

    # Calculate the expected secrets.
    expected = [
        V1Secret(
            api_version="v1",
            kind="Secret",
            data={"token": ANY},
            metadata=V1ObjectMeta(
                name=t["metadata"]["name"],
                namespace=t["metadata"]["namespace"],
                annotations=t["metadata"].get("annotations", {}),
                labels=t["metadata"].get("labels", {}),
                owner_references=[
                    V1OwnerReference(
                        api_version="gafaelfawr.lsst.io/v1alpha1",
                        block_owner_deletion=True,
                        controller=True,
                        kind="GafaelfawrServiceToken",
                        name=t["metadata"]["name"],
                        uid=t["metadata"]["uid"],
                    ),
                ],
            ),
            type="Opaque",
        )
        for t in service_tokens
    ]
    expected = sorted(
        expected, key=lambda o: (o.metadata.namespace, o.metadata.name)
    )
    assert mock.get_all_objects_for_test("Secret") == expected

    # Now check that every token in those secrets is correct.
    for service_token in service_tokens:
        name = service_token["metadata"]["name"]
        namespace = service_token["metadata"]["namespace"]
        secret = await mock.read_namespaced_secret(name, namespace)
        data = await token_data_from_secret(token_service, secret)
        assert data == TokenData(
            token=data.token,
            username=service_token["spec"]["service"],
            token_type=TokenType.service,
            scopes=service_token["spec"]["scopes"],
            created=data.created,
            expires=None,
            name=None,
            uid=None,
            groups=None,
        )
        if is_fresh:
            now = current_datetime()
            assert now - timedelta(seconds=5) <= data.created <= now
예제 #3
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)
예제 #4
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)
예제 #5
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
예제 #6
0
    def _minimum_expiration(self, token_data: TokenData) -> datetime:
        """Determine the minimum expiration for a child token.

        Parameters
        ----------
        token_data : `gafaelfawr.models.token.TokenData`
            The data for the parent token for which a child token was
            requested.

        Returns
        -------
        min_expires : `datetime.datetime`
            The minimum acceptable expiration time for the child token.  If
            no child tokens with at least this expiration time exist, a new
            child token should be created.
        """
        min_expires = current_datetime() + timedelta(
            seconds=self._config.token_lifetime.total_seconds() / 2)
        if token_data.expires and min_expires > token_data.expires:
            min_expires = token_data.expires
        return min_expires
예제 #7
0
async def assert_kubernetes_secrets_match_config(
        setup: SetupTest,
        mock_kubernetes: MockCoreV1Api,
        is_fresh: bool = True) -> None:
    assert setup.config.kubernetes
    token_service = setup.factory.create_token_service()

    expected = [
        V1Secret(
            api_version="v1",
            data={"token": ANY},
            metadata=V1ObjectMeta(
                labels={KUBERNETES_TOKEN_TYPE_LABEL: "service"},
                name=s.secret_name,
                namespace=s.secret_namespace,
            ),
            type="Opaque",
        ) for s in setup.config.kubernetes.service_secrets
    ]
    assert_kubernetes_objects_are(mock_kubernetes, expected)

    for service_secret in setup.config.kubernetes.service_secrets:
        secret = mock_kubernetes.read_namespaced_secret(
            service_secret.secret_name, service_secret.secret_namespace)
        data = await token_data_from_secret(token_service, secret)
        assert data == TokenData(
            token=data.token,
            username=service_secret.service,
            token_type=TokenType.service,
            scopes=service_secret.scopes,
            created=data.created,
            expires=None,
            name=None,
            uid=None,
            groups=None,
        )
        if is_fresh:
            now = current_datetime()
            assert now - timedelta(seconds=5) <= data.created <= now
예제 #8
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
예제 #9
0
async def build_history(
    setup: SetupTest,
) -> List[TokenChangeHistoryEntry]:
    """Perform a bunch of token manipulations and return the history entries.

    Assume that all token manipulations generate the correct history entries,
    since that's tested in other tests.  The only point of this function is to
    build enough history that we can make interesting paginated queries of it.
    """
    token_service = setup.factory.create_token_service()

    user_info_one = TokenUserInfo(username="******")
    token_one = await token_service.create_session_token(
        user_info_one,
        scopes=["exec:test", "read:all", "user:token"],
        ip_address="192.0.2.3",
    )
    token_data_one = await token_service.get_data(token_one)
    assert token_data_one
    await token_service.get_internal_token(
        token_data_one,
        "foo",
        scopes=["exec:test", "read:all"],
        ip_address="192.0.2.4",
    )
    internal_token_one_bar = await token_service.get_internal_token(
        token_data_one, "bar", scopes=["read:all"], ip_address="192.0.2.3"
    )
    token_data_internal_one_bar = await token_service.get_data(
        internal_token_one_bar
    )
    assert token_data_internal_one_bar
    await token_service.get_internal_token(
        token_data_internal_one_bar, "baz", scopes=[], ip_address="10.10.10.10"
    )
    notebook_token_one = await token_service.get_notebook_token(
        token_data_one, ip_address="198.51.100.5"
    )
    token_data_notebook_one = await token_service.get_data(notebook_token_one)
    assert token_data_notebook_one
    await token_service.get_internal_token(
        token_data_notebook_one,
        "foo",
        scopes=["exec:test"],
        ip_address="10.10.10.20",
    )

    user_info_two = TokenUserInfo(username="******")
    token_two = await token_service.create_session_token(
        user_info_two,
        scopes=["read:some", "user:token"],
        ip_address="192.0.2.20",
    )
    token_data_two = await token_service.get_data(token_two)
    assert token_data_two
    user_token_two = await token_service.create_user_token(
        token_data_two,
        token_data_two.username,
        token_name="some token",
        scopes=["read:some", "user:token"],
        ip_address="192.0.2.20",
    )
    token_data_user_two = await token_service.get_data(user_token_two)
    assert token_data_user_two
    await token_service.get_internal_token(
        token_data_user_two,
        "foo",
        scopes=["read:some"],
        ip_address="10.10.10.10",
    )
    assert await token_service.modify_token(
        user_token_two.key,
        token_data_user_two,
        token_data_user_two.username,
        ip_address="192.0.2.20",
        token_name="happy token",
    )

    request = AdminTokenRequest(
        username="******",
        token_type=TokenType.service,
        scopes=["admin:token"],
    )
    service_token = await token_service.create_token_from_admin_request(
        request,
        TokenData.bootstrap_token(),
        ip_address="2001:db8:034a:ea78:4278:4562:6578:9876",
    )
    service_token_data = await token_service.get_data(service_token)
    assert service_token_data
    assert await token_service.modify_token(
        user_token_two.key,
        service_token_data,
        ip_address="2001:db8:034a:ea78:4278:4562:6578:9876",
        scopes=["admin:token", "read:all"],
    )
    assert await token_service.modify_token(
        user_token_two.key,
        service_token_data,
        ip_address="2001:db8:034a:ea78:4278:4562:6578:af42",
        token_name="other name",
        expires=current_datetime() + timedelta(days=30),
        scopes=["read:all"],
    )
    assert await token_service.delete_token(
        token_one.key,
        service_token_data,
        username=token_data_one.username,
        ip_address="2001:db8:034a:ea78:4278:4562:6578:9876",
    )

    # Spread out the timestamps so that we can test date range queries.  Every
    # other entry has the same timestamp as the previous entry to test that
    # queries handle entries with the same timestamp.
    entries = (
        setup.session.query(TokenChangeHistory)
        .order_by(TokenChangeHistory.id)
        .all()
    )
    event_time = current_datetime() - timedelta(seconds=len(entries) * 5)
    with setup.transaction():
        for i, entry in enumerate(entries):
            entry.event_time = event_time
            if i % 2 != 0:
                event_time += timedelta(seconds=5)

    history = token_service.get_change_history(service_token_data)
    assert history.count == 20
    assert len(history.entries) == 20
    return history.entries
예제 #10
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,
        },
    ]
예제 #11
0
def assert_is_now(date: datetime) -> None:
    """Assert that a datetime is reasonably close to the current time."""
    now = current_datetime()
    assert now - timedelta(seconds=5) <= date <= now
예제 #12
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,
        },
    ]
예제 #13
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")
예제 #14
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
예제 #15
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
예제 #16
0
async def test_modify(factory: ComponentFactory) -> None:
    user_info = TokenUserInfo(username="******",
                              name="Example Person",
                              uid=4137)
    token_service = factory.create_token_service()
    async with factory.session.begin():
        session_token = await token_service.create_session_token(
            user_info,
            scopes=["read:all", "user:token"],
            ip_address="127.0.0.1",
        )
    data = await token_service.get_data(session_token)
    assert data
    async with factory.session.begin():
        user_token = await token_service.create_user_token(
            data,
            data.username,
            token_name="some-token",
            scopes=[],
            ip_address="127.0.0.1",
        )

    expires = current_datetime() + timedelta(days=50)
    async with factory.session.begin():
        await token_service.modify_token(
            user_token.key,
            data,
            token_name="happy token",
            ip_address="127.0.0.1",
        )
        await token_service.modify_token(
            user_token.key,
            data,
            scopes=["read:all"],
            expires=expires,
            ip_address="192.168.0.4",
        )
        info = await token_service.get_token_info_unchecked(user_token.key)
    assert info and info == TokenInfo(
        token=user_token.key,
        username="******",
        token_type=TokenType.user,
        token_name="happy token",
        scopes=["read:all"],
        created=info.created,
        expires=expires,
        last_used=None,
        parent=None,
    )
    async with factory.session.begin():
        await token_service.modify_token(
            user_token.key,
            data,
            expires=None,
            no_expire=True,
            ip_address="127.0.4.5",
        )

    async with factory.session.begin():
        history = await token_service.get_change_history(
            data, token=user_token.key, username=data.username)
    assert history.entries == [
        TokenChangeHistoryEntry(
            token=user_token.key,
            username=data.username,
            token_type=TokenType.user,
            token_name="happy token",
            scopes=["read:all"],
            expires=None,
            actor=data.username,
            action=TokenChange.edit,
            old_expires=expires,
            ip_address="127.0.4.5",
            event_time=history.entries[0].event_time,
        ),
        TokenChangeHistoryEntry(
            token=user_token.key,
            username=data.username,
            token_type=TokenType.user,
            token_name="happy token",
            scopes=["read:all"],
            expires=expires,
            actor=data.username,
            action=TokenChange.edit,
            old_scopes=[],
            old_expires=None,
            ip_address="192.168.0.4",
            event_time=history.entries[1].event_time,
        ),
        TokenChangeHistoryEntry(
            token=user_token.key,
            username=data.username,
            token_type=TokenType.user,
            token_name="happy token",
            scopes=[],
            expires=None,
            actor=data.username,
            action=TokenChange.edit,
            old_token_name="some-token",
            ip_address="127.0.0.1",
            event_time=history.entries[2].event_time,
        ),
        TokenChangeHistoryEntry(
            token=user_token.key,
            username=data.username,
            token_type=TokenType.user,
            token_name="some-token",
            scopes=[],
            expires=None,
            actor=data.username,
            action=TokenChange.create,
            ip_address="127.0.0.1",
            event_time=info.created,
        ),
    ]
예제 #17
0
async def test_token_from_admin_request(factory: ComponentFactory) -> None:
    user_info = TokenUserInfo(username="******",
                              name="Example Person",
                              uid=4137)
    token_service = factory.create_token_service()
    async with factory.session.begin():
        token = await token_service.create_session_token(
            user_info, scopes=[], ip_address="127.0.0.1")
    data = await token_service.get_data(token)
    assert data
    expires = current_datetime() + timedelta(days=2)
    request = AdminTokenRequest(
        username="******",
        token_type=TokenType.user,
        token_name="some token",
        scopes=["read:all"],
        expires=expires,
        name="Other User",
        uid=1345,
        groups=[TokenGroup(name="some-group", id=4133)],
    )

    # Cannot create a token via admin request because the authentication
    # information is missing the admin:token scope.
    with pytest.raises(PermissionDeniedError):
        await token_service.create_token_from_admin_request(
            request, data, ip_address="127.0.0.1")

    # Get a token with an appropriate scope.
    async with factory.session.begin():
        session_token = await token_service.create_session_token(
            user_info, scopes=["admin:token"], ip_address="127.0.0.1")
    admin_data = await token_service.get_data(session_token)
    assert admin_data

    # Test a few more errors.
    request.scopes = ["bogus:scope"]
    with pytest.raises(InvalidScopesError):
        await token_service.create_token_from_admin_request(
            request, admin_data, ip_address="127.0.0.1")
    request.scopes = ["read:all"]
    request.expires = current_datetime()
    with pytest.raises(InvalidExpiresError):
        await token_service.create_token_from_admin_request(
            request, admin_data, ip_address="127.0.0.1")
    request.expires = expires

    # Try a successful request.
    async with factory.session.begin():
        token = await token_service.create_token_from_admin_request(
            request, admin_data, ip_address="127.0.0.1")
    user_data = await token_service.get_data(token)
    assert user_data and user_data == TokenData(
        token=token, created=user_data.created, **request.dict())
    assert_is_now(user_data.created)

    async with factory.session.begin():
        history = await token_service.get_change_history(
            admin_data, token=token.key, username=request.username)
    assert history.entries == [
        TokenChangeHistoryEntry(
            token=token.key,
            username=request.username,
            token_type=TokenType.user,
            token_name=request.token_name,
            scopes=["read:all"],
            expires=request.expires,
            actor=admin_data.username,
            action=TokenChange.create,
            ip_address="127.0.0.1",
            event_time=user_data.created,
        )
    ]

    # Non-admins can't see other people's tokens.
    with pytest.raises(PermissionDeniedError):
        await token_service.get_change_history(data,
                                               token=token.key,
                                               username=request.username)

    # Now request a service token with minimal data instead.
    request = AdminTokenRequest(username="******",
                                token_type=TokenType.service)
    async with factory.session.begin():
        token = await token_service.create_token_from_admin_request(
            request, admin_data, ip_address="127.0.0.1")
    service_data = await token_service.get_data(token)
    assert service_data and service_data == TokenData(
        token=token, created=service_data.created, **request.dict())
    assert_is_now(service_data.created)

    async with factory.session.begin():
        history = await token_service.get_change_history(
            admin_data, token=token.key, username=request.username)
    assert history.entries == [
        TokenChangeHistoryEntry(
            token=token.key,
            username=request.username,
            token_type=TokenType.service,
            scopes=[],
            expires=None,
            actor=admin_data.username,
            action=TokenChange.create,
            ip_address="127.0.0.1",
            event_time=service_data.created,
        )
    ]
예제 #18
0
async def test_child_token_lifetime(config: Config,
                                    factory: ComponentFactory) -> None:
    """Test that a new internal token is generated at half its lifetime."""
    session_token_data = await create_session_token(factory)
    token_service = factory.create_token_service()

    # Generate a user token with a lifetime less than half of the default
    # lifetime for an internal token.  This will get us a short-lived internal
    # token that should be ineligible for handing out for a user token that
    # doesn't expire.
    delta = timedelta(minutes=(config.issuer.exp_minutes / 2) - 5)
    expires = current_datetime() + delta
    async with factory.session.begin():
        user_token = await token_service.create_user_token(
            session_token_data,
            session_token_data.username,
            token_name="n",
            expires=expires,
            scopes=[],
            ip_address="127.0.0.1",
        )
    user_token_data = await token_service.get_data(user_token)
    assert user_token_data

    # Get an internal token and ensure we get the same one when we ask again.
    async with factory.session.begin():
        internal_token = await token_service.get_internal_token(
            user_token_data, service="a", scopes=[], ip_address="127.0.0.1")
    internal_token_data = await token_service.get_data(internal_token)
    assert internal_token_data
    assert internal_token_data.expires == user_token_data.expires
    new_internal_token = await token_service.get_internal_token(
        user_token_data, service="a", scopes=[], ip_address="127.0.0.1")
    assert new_internal_token == internal_token

    # Do the same thing with a notebook token.
    async with factory.session.begin():
        notebook_token = await token_service.get_notebook_token(
            user_token_data, ip_address="127.0.0.1")
    notebook_token_data = await token_service.get_data(notebook_token)
    assert notebook_token_data
    assert notebook_token_data.expires == user_token_data.expires
    new_notebook_token = await token_service.get_notebook_token(
        user_token_data, ip_address="127.0.0.1")
    assert new_notebook_token == notebook_token

    # Change the expiration of the user token to longer than the maximum
    # internal token lifetime.
    new_delta = timedelta(minutes=config.issuer.exp_minutes * 2)
    expires = current_datetime() + new_delta
    async with factory.session.begin():
        assert await token_service.modify_token(
            user_token.key,
            session_token_data,
            session_token_data.username,
            ip_address="127.0.0.1",
            expires=expires,
        )
    user_token_data = await token_service.get_data(user_token)
    assert user_token_data

    # Now, request an internal and notebook token.  We should get different
    # ones with a longer expiration.
    async with factory.session.begin():
        new_internal_token = await token_service.get_internal_token(
            user_token_data, service="a", scopes=[], ip_address="127.0.0.1")
    assert new_internal_token != internal_token
    internal_token = new_internal_token
    internal_token_data = await token_service.get_data(internal_token)
    assert internal_token_data
    delta = timedelta(minutes=config.issuer.exp_minutes)
    assert internal_token_data.expires == internal_token_data.created + delta
    async with factory.session.begin():
        new_notebook_token = await token_service.get_notebook_token(
            user_token_data, ip_address="127.0.0.1")
    assert new_notebook_token != notebook_token
    notebook_token = new_notebook_token
    notebook_token_data = await token_service.get_data(notebook_token)
    assert notebook_token_data
    assert notebook_token_data.expires == notebook_token_data.created + delta

    # Change the expiration of the user token to no longer expire.
    async with factory.session.begin():
        assert await token_service.modify_token(
            user_token.key,
            session_token_data,
            session_token_data.username,
            ip_address="127.0.0.1",
            expires=None,
            no_expire=True,
        )
    user_token_data = await token_service.get_data(user_token)
    assert user_token_data

    # Get an internal and notebook token again.  We should get the same ones
    # as last time.
    new_internal_token = await token_service.get_internal_token(
        user_token_data, service="a", scopes=[], ip_address="127.0.0.1")
    assert new_internal_token == internal_token
    new_notebook_token = await token_service.get_notebook_token(
        user_token_data, ip_address="127.0.0.1")
    assert new_notebook_token == notebook_token
예제 #19
0
async def test_internal_token(config: Config,
                              factory: ComponentFactory) -> None:
    user_info = TokenUserInfo(
        username="******",
        name="Example Person",
        uid=4137,
        groups=[TokenGroup(name="foo", id=1000)],
    )
    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",
        )
    data = await token_service.get_data(session_token)
    assert data

    async with factory.session.begin():
        internal_token = await token_service.get_internal_token(
            data,
            service="some-service",
            scopes=["read:all"],
            ip_address="2001:db8::45",
        )
        assert await token_service.get_user_info(internal_token) == user_info
        info = await token_service.get_token_info_unchecked(internal_token.key)
    assert info and info == TokenInfo(
        token=internal_token.key,
        username=user_info.username,
        token_type=TokenType.internal,
        service="some-service",
        scopes=["read:all"],
        created=info.created,
        last_used=None,
        expires=data.expires,
        parent=session_token.key,
    )
    assert_is_now(info.created)

    # Cannot request a scope that the parent token doesn't have.
    with pytest.raises(InvalidScopesError):
        await token_service.get_internal_token(
            data,
            service="some-service",
            scopes=["read:some"],
            ip_address="127.0.0.1",
        )

    # Creating another internal token from the same parent token with the same
    # parameters just returns the same internal token as before.
    new_internal_token = await token_service.get_internal_token(
        data,
        service="some-service",
        scopes=["read:all"],
        ip_address="127.0.0.1",
    )
    assert internal_token == new_internal_token

    # Try again with the cache cleared to force a database lookup.
    await token_service._token_cache.clear()
    async with factory.session.begin():
        new_internal_token = await token_service.get_internal_token(
            data,
            service="some-service",
            scopes=["read:all"],
            ip_address="127.0.0.1",
        )
    assert internal_token == new_internal_token

    async with factory.session.begin():
        history = await token_service.get_change_history(
            data, token=internal_token.key, username=data.username)
    assert history.entries == [
        TokenChangeHistoryEntry(
            token=internal_token.key,
            username=data.username,
            token_type=TokenType.internal,
            parent=data.token.key,
            service="some-service",
            scopes=["read:all"],
            expires=data.expires,
            actor=data.username,
            action=TokenChange.create,
            ip_address="2001:db8::45",
            event_time=info.created,
        )
    ]

    # It's possible we'll have a race condition where two workers both create
    # an internal token at the same time with the same parameters.  Gafaelfawr
    # 3.0.2 had a regression where, once that had happened, it could not
    # retrieve the internal token because it didn't expect multiple results
    # from the query.  Simulate this and make sure it's handled properly.  The
    # easiest way to do this is to use the internals of the token service.
    second_internal_token = Token()
    created = current_datetime()
    expires = created + config.token_lifetime
    internal_token_data = TokenData(
        token=second_internal_token,
        username=data.username,
        token_type=TokenType.internal,
        scopes=["read:all"],
        created=created,
        expires=expires,
        name=data.name,
        email=data.email,
        uid=data.uid,
        groups=data.groups,
    )
    await token_service._token_redis_store.store_data(internal_token_data)
    async with factory.session.begin():
        await token_service._token_db_store.add(internal_token_data,
                                                service="some-service",
                                                parent=data.token.key)
    await token_service._token_cache.clear()
    async with factory.session.begin():
        dup_internal_token = await token_service.get_internal_token(
            data,
            service="some-service",
            scopes=["read:all"],
            ip_address="127.0.0.1",
        )
    assert dup_internal_token in (internal_token, second_internal_token)

    # A different scope or a different service results in a new token.
    async with factory.session.begin():
        new_internal_token = await token_service.get_internal_token(
            data,
            service="some-service",
            scopes=["exec:admin"],
            ip_address="127.0.0.1",
        )
    assert internal_token != new_internal_token
    async with factory.session.begin():
        new_internal_token = await token_service.get_internal_token(
            data,
            service="another-service",
            scopes=["read:all"],
            ip_address="127.0.0.1",
        )
    assert internal_token != new_internal_token

    # Check that the expiration time is capped by creating a user token that
    # doesn't expire and then creating a notebook token from it.  Use this to
    # test a token with empty scopes.
    async with factory.session.begin():
        user_token = await token_service.create_user_token(
            data,
            data.username,
            token_name="some token",
            scopes=["exec:admin"],
            expires=None,
            ip_address="127.0.0.1",
        )
    data = await token_service.get_data(user_token)
    assert data
    async with factory.session.begin():
        new_internal_token = await token_service.get_internal_token(
            data, service="some-service", scopes=[], ip_address="127.0.0.1")
        assert new_internal_token != internal_token
        info = await token_service.get_token_info_unchecked(
            new_internal_token.key)
    assert info and info.scopes == []
    expires = info.created + timedelta(minutes=config.issuer.exp_minutes)
    assert info.expires == expires
예제 #20
0
async def test_user_token(factory: ComponentFactory) -> None:
    user_info = TokenUserInfo(username="******",
                              name="Example Person",
                              uid=4137)
    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",
        )
    data = await token_service.get_data(session_token)
    assert data
    expires = current_datetime() + timedelta(days=2)

    # Scopes are provided not in sorted order to ensure they're sorted when
    # creating the token.
    async with factory.session.begin():
        user_token = await token_service.create_user_token(
            data,
            "example",
            token_name="some-token",
            scopes=["read:all", "exec:admin"],
            expires=expires,
            ip_address="192.168.0.1",
        )
        assert await token_service.get_user_info(user_token) == user_info
        info = await token_service.get_token_info_unchecked(user_token.key)
    assert info and info == TokenInfo(
        token=user_token.key,
        username=user_info.username,
        token_name="some-token",
        token_type=TokenType.user,
        scopes=["exec:admin", "read:all"],
        created=info.created,
        last_used=None,
        expires=int(expires.timestamp()),
        parent=None,
    )
    assert_is_now(info.created)
    assert await token_service.get_data(user_token) == TokenData(
        token=user_token,
        username=user_info.username,
        token_type=TokenType.user,
        scopes=["exec:admin", "read:all"],
        created=info.created,
        expires=info.expires,
        name=user_info.name,
        uid=user_info.uid,
    )

    async with factory.session.begin():
        history = await token_service.get_change_history(
            data, token=user_token.key, username=data.username)
    assert history.entries == [
        TokenChangeHistoryEntry(
            token=user_token.key,
            username=data.username,
            token_type=TokenType.user,
            token_name="some-token",
            scopes=["exec:admin", "read:all"],
            expires=info.expires,
            actor=data.username,
            action=TokenChange.create,
            ip_address="192.168.0.1",
            event_time=info.created,
        )
    ]