Skip to content

Commit

Permalink
Use new IsService from toolkit (#2473)
Browse files Browse the repository at this point in the history
  • Loading branch information
marcoacierno authored Nov 16, 2021
1 parent 7e1b238 commit ff182dd
Show file tree
Hide file tree
Showing 9 changed files with 146 additions and 158 deletions.
2 changes: 1 addition & 1 deletion gateway/pastaporto/user-info.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ export const fetchUserInfo = async (id: string): Promise<User | null> => {

const token: string = jwt.sign({}, SERVICE_TO_SERVICE_SECRET!, {
issuer: "gateway",
audience: "users-service",
audience: "users-backend",
expiresIn: "1m",
});

Expand Down
14 changes: 1 addition & 13 deletions users-backend/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,12 @@
from starlette.applications import Starlette
from starlette.middleware import Middleware
from starlette.middleware.sessions import SessionMiddleware
from starlette.responses import JSONResponse
from starlette.routing import Route

from users.admin_api.views import GraphQL as AdminGraphQL
from users.api.views import GraphQL
from users.db import get_engine, get_session
from users.domain.repository import UsersRepository
from users.internal_api.permissions import is_service
from users.internal_api.views import GraphQL as InternalGraphQL
from users.settings import DEBUG, ENVIRONMENT, PASTAPORTO_SECRET, SECRET_KEY, SENTRY_DSN
from users.social_auth.views import google_login, google_login_auth
Expand Down Expand Up @@ -45,17 +44,6 @@
)


@app.middleware("http")
async def internal_api_middleware(request, call_next):
if request.url.path != "/internal-api":
return await call_next(request)

if not is_service(request):
return JSONResponse({"error": "Forbidden"}, 400)
else:
return await call_next(request)


@app.middleware("http")
async def repositories_middleware(request, call_next):
request.state.users_repository = UsersRepository(request.state.session)
Expand Down
166 changes: 93 additions & 73 deletions users-backend/poetry.lock

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions users-backend/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,11 @@ PyJWT = "2.0.1"
pydantic = "^1.7.3"
email-validator = "^1.1.2"
argon2-cffi = "^20.1.0"
httpx = "0.18.2"
httpx = "0.20.0"
Authlib = "^0.15.3"
itsdangerous = "^1.1.0"
mangum = "^0.10.0"
pythonit-toolkit = "^0.1.55"
pythonit-toolkit = "0.1.59"
graphql-core = "^3.1.5"
sentry-sdk = "^1.1.0"

Expand Down
25 changes: 5 additions & 20 deletions users-backend/users/internal_api/permissions.py
Original file line number Diff line number Diff line change
@@ -1,24 +1,9 @@
import jwt
from pythonit_toolkit.headers import SERVICE_JWT_HEADER
from pythonit_toolkit.pastaporto.tokens import decode_service_to_service_token
from starlette.requests import Request
from pythonit_toolkit.api.permissions import IsService as BaseIsService

from users.settings import SERVICE_TO_SERVICE_SECRET


def is_service(request: Request) -> bool:
token = request.headers.get(SERVICE_JWT_HEADER)
secret = str(SERVICE_TO_SERVICE_SECRET)

try:
decode_service_to_service_token(
token, secret, issuer="gateway", audience="users-service"
)
return True
except (
jwt.DecodeError,
jwt.InvalidIssuerError,
jwt.ExpiredSignatureError,
jwt.InvalidAudienceError,
):
return False
def IsService(allowed_callers: list[str]):
return BaseIsService(
allowed_callers, str(SERVICE_TO_SERVICE_SECRET), "users-backend"
)
3 changes: 2 additions & 1 deletion users-backend/users/internal_api/query.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,15 @@
from strawberry import ID

from users.internal_api.context import Info
from users.internal_api.permissions import IsService
from users.internal_api.types import User

logger = logging.getLogger(__name__)


@strawberry.type
class Query:
@strawberry.field
@strawberry.field(permission_classes=[IsService(["gateway", "pycon-backend"])])
async def user(self, info: Info, id: ID) -> Optional[User]:
logger.info("Internal api request to get user_id=%s information", id)
user = await info.context.users_repository.get_by_id(int(id))
Expand Down
21 changes: 21 additions & 0 deletions users-backend/users/internal_api/tests/queries/test_user_query.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,3 +51,24 @@ async def _(
response = await internalapi_graphql_client.query(query, variables={"id": 100})
assert not response.errors
assert response.data["user"] is None


@test("cannot get user without a service to service token")
async def _(
internalapi_graphql_client=internalapi_graphql_client,
db=db,
user_factory=user_factory,
):
user = await user_factory(email="testuser@user.it", is_staff=False)

query = """query($id: ID!) {
user(id: $id) {
id
email
isStaff
}
}"""

response = await internalapi_graphql_client.query(query, variables={"id": user.id})
assert response.errors[0]["message"] == "Forbidden"
assert not response.data["user"]
30 changes: 0 additions & 30 deletions users-backend/users/internal_api/tests/test_views.py

This file was deleted.

39 changes: 21 additions & 18 deletions users-backend/users/tests/api.py
Original file line number Diff line number Diff line change
@@ -1,38 +1,41 @@
from asgi_lifespan import LifespanManager
from httpx import AsyncClient
from main import app
from pythonit_toolkit.api.graphql_test_client import GraphQLClient
from ward import Scope, fixture

from main import app
from users.settings import PASTAPORTO_SECRET, SERVICE_TO_SERVICE_SECRET
from ward import fixture


@fixture(scope=Scope.Global)
async def client():
async with AsyncClient(app=app, base_url="http://testserver") as async_client:
yield async_client


@fixture
async def testclient():
async def testclient(client=client):
async with LifespanManager(app):
async with AsyncClient(app=app, base_url="http://testserver") as client:
yield client
yield client


@fixture()
async def graphql_client(testclient=testclient):
async with testclient:
yield GraphQLClient(testclient, pastaporto_secret=PASTAPORTO_SECRET)
yield GraphQLClient(testclient, pastaporto_secret=PASTAPORTO_SECRET)


@fixture()
async def admin_graphql_client(testclient=testclient):
async with testclient:
yield GraphQLClient(
testclient, admin_endpoint=True, pastaporto_secret=PASTAPORTO_SECRET
)
yield GraphQLClient(
testclient, admin_endpoint=True, pastaporto_secret=PASTAPORTO_SECRET
)


@fixture()
async def internalapi_graphql_client(testclient=testclient):
async with testclient:
yield GraphQLClient(
testclient,
internal_api_endpoint=True,
pastaporto_secret=PASTAPORTO_SECRET,
service_to_service_secret=SERVICE_TO_SERVICE_SECRET,
)
yield GraphQLClient(
testclient,
internal_api_endpoint=True,
pastaporto_secret=PASTAPORTO_SECRET,
service_to_service_secret=SERVICE_TO_SERVICE_SECRET,
)

0 comments on commit ff182dd

Please sign in to comment.