コード例 #1
0
async def test_verify_oidc(tmp_path: Path, respx_mock: respx.Router,
                           factory: ComponentFactory) -> None:
    config = await configure(tmp_path, "oidc")
    factory.reconfigure(config)
    verifier = factory.create_token_verifier()

    now = datetime.now(timezone.utc)
    exp = now + timedelta(days=24)
    payload: Dict[str, Any] = {
        "aud": config.verifier.oidc_aud,
        "iat": int(now.timestamp()),
        "exp": int(exp.timestamp()),
    }
    keypair = config.issuer.keypair
    token = encode_token(payload, keypair)
    excinfo: ExceptionInfo[Exception]

    # Missing iss.
    with pytest.raises(InvalidIssuerError) as excinfo:
        await verifier.verify_oidc_token(token)
    assert str(excinfo.value) == "No iss claim in token"

    # Missing kid.
    payload["iss"] = "https://bogus.example.com/"
    token = encode_token(payload, keypair)
    with pytest.raises(UnknownKeyIdException) as excinfo:
        await verifier.verify_oidc_token(token)
    assert str(excinfo.value) == "No kid in token header"

    # Unknown issuer.
    token = encode_token(payload, keypair, kid="a-kid")
    with pytest.raises(InvalidIssuerError) as excinfo:
        await verifier.verify_oidc_token(token)
    assert str(excinfo.value) == "Unknown issuer: https://bogus.example.com/"

    # Unknown kid.
    payload["iss"] = config.verifier.oidc_iss
    token = encode_token(payload, keypair, kid="a-kid")
    with pytest.raises(UnknownKeyIdException) as excinfo:
        await verifier.verify_oidc_token(token)
    expected = f"kid a-kid not allowed for {config.verifier.oidc_iss}"
    assert str(excinfo.value) == expected

    # Missing username claim.
    await mock_oidc_provider_config(respx_mock, keypair)
    kid = config.verifier.oidc_kids[0]
    token = encode_token(payload, config.issuer.keypair, kid=kid)
    with pytest.raises(MissingClaimsException) as excinfo:
        await verifier.verify_oidc_token(token)
    expected = f"No {config.verifier.username_claim} claim in token"
    assert str(excinfo.value) == expected

    # Missing UID claim.
    await mock_oidc_provider_config(respx_mock, keypair)
    payload[config.verifier.username_claim] = "some-user"
    token = encode_token(payload, config.issuer.keypair, kid=kid)
    with pytest.raises(MissingClaimsException) as excinfo:
        await verifier.verify_oidc_token(token)
    expected = f"No {config.verifier.uid_claim} claim in token"
    assert str(excinfo.value) == expected
コード例 #2
0
ファイル: oidc_test.py プロジェクト: lsst-sqre/gafaelfawr
async def test_redeem_code_errors(tmp_path: Path,
                                  factory: ComponentFactory) -> None:
    clients = [
        OIDCClient(client_id="client-1", client_secret="client-1-secret"),
        OIDCClient(client_id="client-2", client_secret="client-2-secret"),
    ]
    config = await configure(tmp_path, "github", oidc_clients=clients)
    factory.reconfigure(config)
    oidc_service = factory.create_oidc_service()
    token_data = await create_session_token(factory)
    token = token_data.token
    redirect_uri = "https://example.com/"
    code = await oidc_service.issue_code("client-2", redirect_uri, token)

    with pytest.raises(InvalidClientError):
        await oidc_service.redeem_code("some-client", "some-secret",
                                       redirect_uri, code)
    with pytest.raises(InvalidClientError):
        await oidc_service.redeem_code("client-2", "some-secret", redirect_uri,
                                       code)
    with pytest.raises(InvalidGrantError):
        await oidc_service.redeem_code(
            "client-2",
            "client-2-secret",
            redirect_uri,
            OIDCAuthorizationCode(),
        )
    with pytest.raises(InvalidGrantError):
        await oidc_service.redeem_code("client-1", "client-1-secret",
                                       redirect_uri, code)
    with pytest.raises(InvalidGrantError):
        await oidc_service.redeem_code("client-2", "client-2-secret",
                                       "https://foo.example.com/", code)
コード例 #3
0
ファイル: influxdb_test.py プロジェクト: lsst-sqre/gafaelfawr
async def test_not_configured(
    tmp_path: Path,
    client: AsyncClient,
    factory: ComponentFactory,
    caplog: LogCaptureFixture,
) -> None:
    config = await configure(tmp_path, "oidc")
    factory.reconfigure(config)
    token_data = await create_session_token(factory)

    caplog.clear()
    r = await client.get(
        "/auth/tokens/influxdb/new",
        headers={"Authorization": f"bearer {token_data.token}"},
    )

    assert r.status_code == 404
    assert r.json()["detail"]["type"] == "not_supported"

    assert parse_log(caplog) == [{
        "error": "No InfluxDB issuer configuration",
        "event": "Not configured",
        "httpRequest": {
            "requestMethod": "GET",
            "requestUrl":
            (f"https://{TEST_HOSTNAME}/auth/tokens/influxdb/new"),
            "remoteIp": "127.0.0.1",
        },
        "scope": "user:token",
        "severity": "warning",
        "token": token_data.token.key,
        "token_source": "bearer",
        "user": token_data.username,
    }]
コード例 #4
0
async def test_issue_token(tmp_path: Path, factory: ComponentFactory) -> None:
    config = await configure(tmp_path, "oidc")
    factory.reconfigure(config)
    issuer = factory.create_token_issuer()

    token_data = await create_session_token(factory)
    oidc_token = issuer.issue_token(token_data, jti="new-jti", scope="openid")

    assert oidc_token.claims == {
        "aud": config.issuer.aud,
        "exp": ANY,
        "iat": ANY,
        "iss": config.issuer.iss,
        "jti": "new-jti",
        "name": token_data.name,
        "preferred_username": token_data.username,
        "scope": "openid",
        "sub": token_data.username,
        config.issuer.username_claim: token_data.username,
        config.issuer.uid_claim: token_data.uid,
    }

    now = time.time()
    assert now - 5 <= oidc_token.claims["iat"] <= now + 5
    expected_exp = now + config.issuer.exp_minutes * 60
    assert expected_exp - 5 <= oidc_token.claims["exp"] <= expected_exp + 5
コード例 #5
0
ファイル: oidc_test.py プロジェクト: lsst-sqre/gafaelfawr
async def test_redeem_code(tmp_path: Path, factory: ComponentFactory) -> None:
    clients = [
        OIDCClient(client_id="client-1", client_secret="client-1-secret"),
        OIDCClient(client_id="client-2", client_secret="client-2-secret"),
    ]
    config = await configure(tmp_path, "github", oidc_clients=clients)
    factory.reconfigure(config)
    oidc_service = factory.create_oidc_service()
    token_data = await create_session_token(factory)
    token = token_data.token
    redirect_uri = "https://example.com/"
    code = await oidc_service.issue_code("client-2", redirect_uri, token)

    oidc_token = await oidc_service.redeem_code("client-2", "client-2-secret",
                                                redirect_uri, code)
    assert oidc_token.claims == {
        "aud": config.issuer.aud,
        "iat": ANY,
        "exp": ANY,
        "iss": config.issuer.iss,
        "jti": code.key,
        "name": token_data.name,
        "preferred_username": token_data.username,
        "scope": "openid",
        "sub": token_data.username,
        config.issuer.username_claim: token_data.username,
        config.issuer.uid_claim: token_data.uid,
    }

    redis = await redis_dependency()
    assert not await redis.get(f"oidc:{code.key}")
コード例 #6
0
async def test_key_retrieval(tmp_path: Path, respx_mock: respx.Router,
                             factory: ComponentFactory) -> None:
    config = await configure(tmp_path, "oidc-no-kids")
    factory.reconfigure(config)
    assert config.oidc
    verifier = factory.create_token_verifier()

    # Initial working JWKS configuration.
    jwks = config.issuer.keypair.public_key_as_jwks("some-kid")

    # Register that handler at the well-known JWKS endpoint.  This will return
    # a connection refused from the OpenID Connect endpoint.
    jwks_url = urljoin(config.oidc.issuer, "/.well-known/jwks.json")
    oidc_url = urljoin(config.oidc.issuer, "/.well-known/openid-configuration")
    respx_mock.get(jwks_url).respond(json=jwks.dict())
    respx_mock.get(oidc_url).respond(404)

    # Check token verification with this configuration.
    token = await create_upstream_oidc_token(kid="some-kid")
    assert await verifier.verify_oidc_token(token)

    # Wrong algorithm for the key.
    jwks.keys[0].alg = "ES256"
    respx_mock.get(jwks_url).respond(json=jwks.dict())
    with pytest.raises(UnknownAlgorithmException):
        await verifier.verify_oidc_token(token)

    # Should go back to working if we fix the algorithm and add more keys.
    # Add an explicit 404 from the OpenID connect endpoint.
    respx_mock.get(oidc_url).respond(404)
    jwks.keys[0].alg = ALGORITHM
    keypair = RSAKeyPair.generate()
    jwks.keys.insert(0, keypair.public_key_as_jwks("a-kid").keys[0])
    respx_mock.get(jwks_url).respond(json=jwks.dict())
    assert await verifier.verify_oidc_token(token)

    # Try with a new key ID and return a malformed reponse.
    respx_mock.get(jwks_url).respond(json=["foo"])
    token = await create_upstream_oidc_token(kid="malformed")
    with pytest.raises(FetchKeysException):
        await verifier.verify_oidc_token(token)

    # Return a 404 error.
    respx_mock.get(jwks_url).respond(404)
    with pytest.raises(FetchKeysException):
        await verifier.verify_oidc_token(token)

    # Fix the JWKS handler but register a malformed URL as the OpenID Connect
    # configuration endpoint, which should be checked first.
    jwks.keys[1].kid = "another-kid"
    respx_mock.get(jwks_url).respond(json=jwks.dict())
    respx_mock.get(oidc_url).respond(json=["foo"])
    token = await create_upstream_oidc_token(kid="another-kid")
    with pytest.raises(FetchKeysException):
        await verifier.verify_oidc_token(token)

    # Try again with a working OpenID Connect configuration.
    respx_mock.get(oidc_url).respond(json={"jwks_uri": jwks_url})
    assert await verifier.verify_oidc_token(token)
コード例 #7
0
async def test_no_scope(
    client: AsyncClient, factory: ComponentFactory
) -> None:
    token_data = await create_session_token(factory)
    username = token_data.username
    token_service = factory.create_token_service()
    async with factory.session.begin():
        token = await token_service.create_user_token(
            token_data,
            token_data.username,
            token_name="user",
            scopes=[],
            ip_address="127.0.0.1",
        )

    r = await client.get(
        f"/auth/api/v1/users/{username}/token-change-history",
        headers={"Authorization": f"bearer {token}"},
    )
    assert r.status_code == 403

    r = await client.get(
        f"/auth/api/v1/users/{username}/tokens/{token.key}/change-history",
        headers={"Authorization": f"bearer {token}"},
    )
    assert r.status_code == 403
コード例 #8
0
async def test_admins(client: AsyncClient, factory: ComponentFactory) -> None:
    r = await client.get("/auth/api/v1/admins")
    assert r.status_code == 401

    token_data = await create_session_token(factory)
    r = await client.get(
        "/auth/api/v1/admins",
        headers={"Authorization": f"bearer {token_data.token}"},
    )
    assert r.status_code == 403
    assert r.json()["detail"][0] == {
        "msg": "Token does not have required scope admin:token",
        "type": "permission_denied",
    }

    token_data = await create_session_token(factory, scopes=["admin:token"])
    r = await client.get(
        "/auth/api/v1/admins",
        headers={"Authorization": f"bearer {token_data.token}"},
    )
    assert r.status_code == 200
    assert r.json() == [{"username": "******"}]

    admin_service = factory.create_admin_service()
    async with factory.session.begin():
        await admin_service.add_admin(
            "example", actor="admin", ip_address="127.0.0.1"
        )

    r = await client.get(
        "/auth/api/v1/admins",
        headers={"Authorization": f"bearer {token_data.token}"},
    )
    assert r.status_code == 200
    assert r.json() == [{"username": "******"}, {"username": "******"}]
コード例 #9
0
ファイル: admin_test.py プロジェクト: lsst-sqre/gafaelfawr
async def test_add(factory: ComponentFactory) -> None:
    admin_service = factory.create_admin_service()

    async with factory.session.begin():
        assert await admin_service.get_admins() == [Admin(username="******")]
        await admin_service.add_admin(
            "example", actor="admin", ip_address="192.168.0.1"
        )

    async with factory.session.begin():
        assert await admin_service.get_admins() == [
            Admin(username="******"),
            Admin(username="******"),
        ]
        assert await admin_service.is_admin("example")
        assert not await admin_service.is_admin("foo")

    async with factory.session.begin():
        with pytest.raises(PermissionDeniedError):
            await admin_service.add_admin(
                "foo", actor="bar", ip_address="127.0.0.1"
            )

    async with factory.session.begin():
        await admin_service.add_admin(
            "foo", actor="<bootstrap>", ip_address="127.0.0.1"
        )

    async with factory.session.begin():
        assert await admin_service.is_admin("foo")
        assert not await admin_service.is_admin("<bootstrap>")
コード例 #10
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
コード例 #11
0
ファイル: conftest.py プロジェクト: lsst-sqre/gafaelfawr
async def factory(empty_database: None) -> AsyncIterator[ComponentFactory]:
    """Return a component factory.

    Note that this creates a separate SQLAlchemy AsyncSession from any that
    may be created by the FastAPI app.
    """
    async with ComponentFactory.standalone() as factory:
        yield factory
コード例 #12
0
 async def check_database() -> None:
     async with ComponentFactory.standalone() as factory:
         admin_service = factory.create_admin_service()
         expected = [Admin(username=u) for u in config.initial_admins]
         assert await admin_service.get_admins() == expected
         token_service = factory.create_token_service()
         bootstrap = TokenData.bootstrap_token()
         assert await token_service.list_tokens(bootstrap) == []
コード例 #13
0
ファイル: token_test.py プロジェクト: lsst-sqre/gafaelfawr
async def test_invalid_username(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", "admin:token"],
            ip_address="127.0.0.1",
        )
    data = await token_service.get_data(session_token)
    assert data

    # Cannot create any type of token with an invalid name.
    for user in (
            "<bootstrap>",
            "<internal>",
            "in+valid",
            " invalid",
            "invalid ",
            "in/valid",
            "in@valid",
            "-invalid",
            "invalid-",
            "in--valid",
    ):
        user_info.username = user
        with pytest.raises(PermissionDeniedError):
            await token_service.create_session_token(user_info,
                                                     scopes=[],
                                                     ip_address="127.0.0.1")
        data.username = user
        with pytest.raises(PermissionDeniedError):
            await token_service.create_user_token(data,
                                                  user,
                                                  token_name="n",
                                                  scopes=[],
                                                  ip_address="127.0.0.1")
        with pytest.raises(PermissionDeniedError):
            await token_service.get_notebook_token(data,
                                                   ip_address="127.0.0.1")
        with pytest.raises(PermissionDeniedError):
            await token_service.get_internal_token(data,
                                                   service="s",
                                                   scopes=[],
                                                   ip_address="127.0.0.1")
        with pytest.raises(ValidationError):
            AdminTokenRequest(username=user, token_type=TokenType.service)
        request = AdminTokenRequest(username="******",
                                    token_type=TokenType.service)
        request.username = user
        with pytest.raises(PermissionDeniedError):
            await token_service.create_token_from_admin_request(
                request, data, ip_address="127.0.0.1")
コード例 #14
0
ファイル: influxdb_test.py プロジェクト: lsst-sqre/gafaelfawr
async def test_influxdb_force_username(
    tmp_path: Path,
    client: AsyncClient,
    factory: ComponentFactory,
    caplog: LogCaptureFixture,
) -> None:
    config = await configure(tmp_path, "influxdb-username")
    factory.reconfigure(config)
    token_data = await create_session_token(factory)
    assert token_data.expires
    influxdb_secret = config.issuer.influxdb_secret
    assert influxdb_secret

    caplog.clear()
    r = await client.get(
        "/auth/tokens/influxdb/new",
        headers={"Authorization": f"bearer {token_data.token}"},
    )

    assert r.status_code == 200
    data = r.json()
    claims = jwt.decode(data["token"], influxdb_secret, algorithms=["HS256"])
    assert claims == {
        "username": "******",
        "exp": int(token_data.expires.timestamp()),
        "iat": ANY,
    }

    assert parse_log(caplog) == [{
        "event": "Issued InfluxDB token",
        "influxdb_username": "******",
        "httpRequest": {
            "requestMethod": "GET",
            "requestUrl":
            (f"https://{TEST_HOSTNAME}/auth/tokens/influxdb/new"),
            "remoteIp": "127.0.0.1",
        },
        "scope": "user:token",
        "severity": "info",
        "token": token_data.token.key,
        "token_source": "bearer",
        "user": token_data.username,
    }]
コード例 #15
0
ファイル: token_test.py プロジェクト: lsst-sqre/gafaelfawr
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)
コード例 #16
0
async def test_basic(factory: ComponentFactory) -> None:
    token_data = await create_session_token(factory, scopes=["read:all"])
    token_service = factory.create_token_service()
    token_cache = factory.create_token_cache_service()
    async with factory.session.begin():
        internal_token = await token_service.get_internal_token(
            token_data, "some-service", ["read:all"], ip_address="127.0.0.1")
        notebook_token = await token_service.get_notebook_token(
            token_data, ip_address="127.0.0.1")

    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")

    # Requesting different internal tokens doesn't work.
    async with factory.session.begin():
        assert internal_token != await token_cache.get_internal_token(
            token_data, "other-service", ["read:all"], "127.0.0.1")
        assert notebook_token != await token_cache.get_internal_token(
            token_data, "some-service", [], "127.0.0.1")

    # A different service token for the same user requesting the same
    # information creates a different internal token.
    new_token_data = await create_session_token(factory, scopes=["read:all"])
    async with factory.session.begin():
        assert internal_token != await token_cache.get_internal_token(
            new_token_data, "some-service", ["read:all"], "127.0.0.1")
        assert notebook_token != await token_cache.get_notebook_token(
            new_token_data, "127.0.0.1")

    # Changing the scope of the parent token doesn't matter as long as the
    # internal token is requested with the same scope.  Cases where the parent
    # token no longer has that scope are caught one level up by the token
    # service and thus aren't tested here.
    token_data.scopes = ["read:all", "admin:token"]
    assert internal_token == await token_cache.get_internal_token(
        token_data, "some-service", ["read:all"], "127.0.0.1")
    async with factory.session.begin():
        assert internal_token != await token_cache.get_internal_token(
            token_data, "some-service", ["admin:token"], "127.0.0.1")
コード例 #17
0
ファイル: token_test.py プロジェクト: lsst-sqre/gafaelfawr
async def test_list(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=["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",
        )
        other_user_info = TokenUserInfo(username="******",
                                        name="Other Person",
                                        uid=1313)
        other_session_token = await token_service.create_session_token(
            other_user_info, scopes=["admin:token"], ip_address="1.1.1.1")
    admin_data = await token_service.get_data(other_session_token)
    assert admin_data

    async with factory.session.begin():
        session_info = await token_service.get_token_info_unchecked(
            session_token.key)
        assert session_info
        user_token_info = await token_service.get_token_info_unchecked(
            user_token.key)
        assert user_token_info
        other_session_info = await token_service.get_token_info_unchecked(
            other_session_token.key)
        assert other_session_info
        assert await token_service.list_tokens(data, "example") == sorted(
            sorted((session_info, user_token_info), key=lambda t: t.token),
            key=lambda t: t.created,
            reverse=True,
        )
        assert await token_service.list_tokens(admin_data) == sorted(
            sorted(
                (session_info, other_session_info, user_token_info),
                key=lambda t: t.token,
            ),
            key=lambda t: t.created,
            reverse=True,
        )

    # Regular users can't retrieve all tokens.
    with pytest.raises(PermissionDeniedError):
        await token_service.list_tokens(data)
コード例 #18
0
async def test_verify_oidc_no_kids(tmp_path: Path, respx_mock: respx.Router,
                                   factory: ComponentFactory) -> None:
    config = await configure(tmp_path, "oidc-no-kids")
    factory.reconfigure(config)
    keypair = config.issuer.keypair
    verifier = factory.create_token_verifier()
    await mock_oidc_provider_config(respx_mock, keypair, "kid")

    now = datetime.now(timezone.utc)
    exp = now + timedelta(days=24)
    payload: Dict[str, Any] = {
        "aud": config.verifier.oidc_aud,
        "iat": int(now.timestamp()),
        "iss": config.verifier.oidc_iss,
        "exp": int(exp.timestamp()),
    }
    token = encode_token(payload, keypair, kid="a-kid")
    with pytest.raises(UnknownKeyIdException) as excinfo:
        await verifier.verify_oidc_token(token)
    expected = f"Issuer {config.verifier.oidc_iss} has no kid a-kid"
    assert str(excinfo.value) == expected
コード例 #19
0
async def test_userinfo(client: AsyncClient,
                        factory: ComponentFactory) -> None:
    token_data = await create_session_token(factory)
    issuer = factory.create_token_issuer()
    oidc_token = issuer.issue_token(token_data, jti="some-jti")

    r = await client.get(
        "/auth/userinfo",
        headers={"Authorization": f"Bearer {oidc_token.encoded}"},
    )

    assert r.status_code == 200
    assert r.json() == oidc_token.claims
コード例 #20
0
    def factory(self) -> ComponentFactory:
        """A factory for constructing Gafaelfawr components.

        This is constructed on the fly at each reference to ensure that we get
        the latest logger, which may have additional bound context.
        """
        return ComponentFactory(
            config=self.config,
            redis=self.redis,
            session=db.session,
            http_client=self.http_client,
            logger=self.logger,
        )
コード例 #21
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
コード例 #22
0
ファイル: oidc_test.py プロジェクト: lsst-sqre/gafaelfawr
async def test_issue_code(tmp_path: Path, factory: ComponentFactory) -> None:
    clients = [OIDCClient(client_id="some-id", client_secret="some-secret")]
    config = await configure(tmp_path, "github", oidc_clients=clients)
    factory.reconfigure(config)
    oidc_service = factory.create_oidc_service()
    token_data = await create_session_token(factory)
    token = token_data.token
    redirect_uri = "https://example.com/"

    assert config.oidc_server
    assert list(config.oidc_server.clients) == clients

    with pytest.raises(UnauthorizedClientException):
        await oidc_service.issue_code("unknown-client", redirect_uri, token)

    code = await oidc_service.issue_code("some-id", redirect_uri, token)
    redis = await redis_dependency()
    encrypted_code = await redis.get(f"oidc:{code.key}")
    assert encrypted_code
    fernet = Fernet(config.session_secret.encode())
    serialized_code = json.loads(fernet.decrypt(encrypted_code))
    assert serialized_code == {
        "code": {
            "key": code.key,
            "secret": code.secret,
        },
        "client_id": "some-id",
        "redirect_uri": redirect_uri,
        "token": {
            "key": token.key,
            "secret": token.secret,
        },
        "created_at": ANY,
    }
    now = time.time()
    assert now - 2 < serialized_code["created_at"] < now
コード例 #23
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")
コード例 #24
0
ファイル: cli.py プロジェクト: yee379/gafaelfawr
async def update_service_tokens() -> None:
    """Update service tokens stored in Kubernetes secrets."""
    config = config_dependency()
    logger = structlog.get_logger(config.safir.logger_name)
    if not config.kubernetes:
        logger.info("No Kubernetes secrets configured")
        sys.exit(0)
    async with ComponentFactory.standalone() as factory:
        kubernetes_service = factory.create_kubernetes_service()
        try:
            await kubernetes_service.update_service_secrets()
        except KubernetesError as e:
            msg = "Failed to update service token secrets"
            logger.error(msg, error=str(e))
            sys.exit(1)
コード例 #25
0
ファイル: tokens.py プロジェクト: lsst-sqre/gafaelfawr
async def create_session_token(
    factory: ComponentFactory,
    *,
    username: Optional[str] = None,
    group_names: Optional[List[str]] = None,
    scopes: Optional[List[str]] = None,
) -> TokenData:
    """Create a session token.

    Parameters
    ----------
    factory : `gafaelfawr.factory.ComponentFactory`
        Factory used to create services to add the token.
    username : `str`, optional
        Override the username of the generated token.
    group_namess : List[`str`], optional
        Group memberships the generated token should have.
    scopes : List[`str`], optional
        Scope for the generated token.

    Returns
    -------
    data : `gafaelfawr.models.token.TokenData`
        The data for the generated token.
    """
    if not username:
        username = "******"
    if group_names:
        groups = [TokenGroup(name=g, id=1000) for g in group_names]
    else:
        groups = []
    user_info = TokenUserInfo(
        username=username,
        name="Some User",
        email="*****@*****.**",
        uid=1000,
        groups=groups,
    )
    if not scopes:
        scopes = ["user:token"]
    token_service = factory.create_token_service()
    async with factory.session.begin():
        token = await token_service.create_session_token(
            user_info, scopes=scopes, ip_address="127.0.0.1"
        )
    data = await token_service.get_data(token)
    assert data
    return data
コード例 #26
0
async def test_errors_scope(
    factory: ComponentFactory, mock_kubernetes: MockKubernetesApi
) -> None:
    await mock_kubernetes.create_namespaced_custom_object(
        "gafaelfawr.lsst.io",
        "v1alpha1",
        "mobu",
        "gafaelfawrservicetokens",
        {
            "apiVersion": "gafaelfawr.lsst.io/v1alpha1",
            "kind": "GafaelfawrServiceToken",
            "metadata": {
                "name": "gafaelfawr-secret",
                "namespace": "mobu",
                "generation": 1,
            },
            "spec": {
                "service": "mobu",
                "scopes": ["invalid:scope"],
            },
        },
    )
    kubernetes_service = factory.create_kubernetes_service(MagicMock())

    await kubernetes_service.update_service_tokens()
    with pytest.raises(ApiException):
        await mock_kubernetes.read_namespaced_secret(
            "gafaelfawr-secret", "mobu"
        )
    service_token = await mock_kubernetes.get_namespaced_custom_object(
        "gafaelfawr.lsst.io",
        "v1alpha1",
        "mobu",
        "gafaelfawrservicetokens",
        "gafaelfawr-secret",
    )
    assert service_token["status"]["conditions"] == [
        {
            "lastTransitionTime": ANY,
            "message": "Unknown scopes requested",
            "observedGeneration": 1,
            "reason": StatusReason.Failed.value,
            "status": "False",
            "type": "SecretCreated",
        }
    ]
コード例 #27
0
ファイル: setup.py プロジェクト: yee379/gafaelfawr
    def factory(self) -> ComponentFactory:
        """Return a `~gafaelfawr.factory.ComponentFactory`.

        Build a new one each time to ensure that it picks up the current
        configuration information.

        Returns
        -------
        factory : `gafaelfawr.factory.ComponentFactory`
            Newly-created factory.
        """
        return ComponentFactory(
            config=self.config,
            redis=self.redis,
            http_client=self.client,
            session=self.session,
            logger=self.logger,
        )
コード例 #28
0
async def test_github_admin(client: AsyncClient, respx_mock: respx.Router,
                            factory: ComponentFactory) -> None:
    """Test that a token administrator gets the admin:token scope."""
    admin_service = factory.create_admin_service()
    async with factory.session.begin():
        await admin_service.add_admin("someuser",
                                      actor="admin",
                                      ip_address="127.0.0.1")
    user_info = GitHubUserInfo(
        name="A User",
        username="******",
        uid=1000,
        email="*****@*****.**",
        teams=[GitHubTeam(slug="a-team", gid=1000, organization="ORG")],
    )

    r = await simulate_github_login(client, respx_mock, user_info)
    assert r.status_code == 307

    # The user should have admin:token scope.
    r = await client.get("/auth", params={"scope": "admin:token"})
    assert r.status_code == 200
コード例 #29
0
async def _selenium_startup(token_path: str) -> None:
    """Startup hook for the app run in Selenium testing mode."""
    config = await config_dependency()
    user_info = TokenUserInfo(username="******", name="Test User", uid=1000)
    scopes = list(config.known_scopes.keys())

    async with ComponentFactory.standalone() as factory:
        async with factory.session.begin():
            # Add an expired token so that we can test display of expired
            # tokens.
            await add_expired_session_token(
                user_info,
                scopes=scopes,
                ip_address="127.0.0.1",
                session=factory.session,
            )

            # Add the valid session token.
            token_service = factory.create_token_service()
            token = await token_service.create_session_token(
                user_info, scopes=scopes, ip_address="127.0.0.1")

    with open(token_path, "w") as f:
        f.write(str(token))
コード例 #30
0
async def test_wrong_user(
    client: AsyncClient, factory: ComponentFactory
) -> None:
    token_data = await create_session_token(factory)
    csrf = await set_session_cookie(client, token_data.token)
    token_service = factory.create_token_service()
    user_info = TokenUserInfo(
        username="******", name="Some Other Person", uid=137123
    )
    async with factory.session.begin():
        other_session_token = await token_service.create_session_token(
            user_info, scopes=["user:token"], ip_address="127.0.0.1"
        )
    other_session_data = await token_service.get_data(other_session_token)
    assert other_session_data
    async with factory.session.begin():
        other_token = await token_service.create_user_token(
            other_session_data,
            "other-person",
            token_name="foo",
            scopes=[],
            ip_address="127.0.0.1",
        )

    # Get a token list.
    r = await client.get("/auth/api/v1/users/other-person/tokens")
    assert r.status_code == 403
    assert r.json()["detail"][0]["type"] == "permission_denied"

    # Create a new user token.
    r = await client.post(
        "/auth/api/v1/users/other-person/tokens",
        headers={"X-CSRF-Token": csrf},
        json={"token_name": "happy token"},
    )
    assert r.status_code == 403
    assert r.json()["detail"][0]["type"] == "permission_denied"

    # Get an individual token.
    r = await client.get(
        f"/auth/api/v1/users/other-person/tokens/{other_token.key}"
    )
    assert r.status_code == 403
    assert r.json()["detail"][0]["type"] == "permission_denied"

    # Get the history of an individual token.
    r = await client.get(
        f"/auth/api/v1/users/other-person/tokens/{other_token.key}"
        "/change-history"
    )
    assert r.status_code == 403
    assert r.json()["detail"][0]["type"] == "permission_denied"

    # Ensure you can't see someone else's token under your username either.
    r = await client.get(
        f"/auth/api/v1/users/{token_data.username}/tokens/{other_token.key}"
    )
    assert r.status_code == 404

    # Or their history.
    r = await client.get(
        f"/auth/api/v1/users/{token_data.username}/tokens/{other_token.key}"
        "/change-history"
    )
    assert r.status_code == 404

    # Delete a token.
    r = await client.delete(
        f"/auth/api/v1/users/other-person/tokens/{other_token.key}",
        headers={"X-CSRF-Token": csrf},
    )
    assert r.status_code == 403
    assert r.json()["detail"][0]["type"] == "permission_denied"
    r = await client.delete(
        f"/auth/api/v1/users/{token_data.username}/tokens/{other_token.key}",
        headers={"X-CSRF-Token": csrf},
    )
    assert r.status_code == 404

    # Modify a token.
    r = await client.patch(
        f"/auth/api/v1/users/other-person/tokens/{other_token.key}",
        json={"token_name": "happy token"},
        headers={"X-CSRF-Token": csrf},
    )
    assert r.status_code == 403
    assert r.json()["detail"][0]["type"] == "permission_denied"
    r = await client.patch(
        f"/auth/api/v1/users/{token_data.username}/tokens/{other_token.key}",
        json={"token_name": "happy token"},
        headers={"X-CSRF-Token": csrf},
    )
    assert r.status_code == 404