Skip to content

Commit

Permalink
server: Disable user & organization API for blocked resources
Browse files Browse the repository at this point in the history
  • Loading branch information
birkjernstrom committed May 14, 2024
1 parent 93609e9 commit 748d21a
Show file tree
Hide file tree
Showing 7 changed files with 215 additions and 4 deletions.
5 changes: 5 additions & 0 deletions server/polar/auth/dependencies.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,11 @@ def __call__(self, auth_subject: AuthSubject[Subject]) -> AuthSubject[Subject]:
else:
raise Unauthorized()

# Blocked subjects
blocked_at = getattr(auth_subject.subject, "blocked_at", None)
if blocked_at is not None:
raise Unauthorized()

# Not allowed subject
subject_type = type(auth_subject.subject)
if subject_type not in self.allowed_subjects:
Expand Down
30 changes: 27 additions & 3 deletions server/polar/organization/service.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,26 +46,49 @@ class OrganizationService(ResourceServiceReader[Organization]):
async def list_installed(self, session: AsyncSession) -> Sequence[Organization]:
stmt = sql.select(Organization).where(
Organization.deleted_at.is_(None),
Organization.blocked_at.is_(None),
Organization.installation_id.is_not(None),
)
res = await session.execute(stmt)
return res.scalars().all()

# Override get method to include `blocked_at` filter
async def get(
self, session: AsyncSession, id: UUID, allow_deleted: bool = False
) -> Organization | None:
conditions = [Organization.id == id]
if not allow_deleted:
conditions.append(Organization.deleted_at.is_(None))

conditions.append(Organization.blocked_at.is_(None))
query = sql.select(Organization).where(*conditions)
res = await session.execute(query)
return res.scalars().unique().one_or_none()

async def get_by_platform(
self, session: AsyncSession, platform: Platforms, external_id: int
) -> Organization | None:
return await self.get_by(session, platform=platform, external_id=external_id)
# TODO: Also add deleted_at=None in a separate commit
return await self.get_by(
session,
platform=platform,
external_id=external_id,
blocked_at=None,
)

async def get_by_name(
self, session: AsyncSession, platform: Platforms, name: str
) -> Organization | None:
return await self.get_by(session, platform=platform, name=name)
# TODO: Also add deleted_at=None in a separate commit
return await self.get_by(session, platform=platform, name=name, blocked_at=None)

async def get_by_custom_domain(
self, session: AsyncSession, custom_domain: str
) -> Organization | None:
# TODO: Also add deleted_at=None in a separate commit
query = sql.select(Organization).where(
Organization.custom_domain == custom_domain
Organization.custom_domain == custom_domain,
Organization.blocked_at.is_(None),
)
res = await session.execute(query)
return res.scalars().unique().one_or_none()
Expand All @@ -78,6 +101,7 @@ async def get_personal(
.join(UserOrganization)
.where(
Organization.deleted_at.is_(None),
Organization.blocked_at.is_(None),
Organization.is_personal.is_(True),
UserOrganization.user_id == user_id,
UserOrganization.deleted_at.is_(None),
Expand Down
3 changes: 3 additions & 0 deletions server/polar/user/service.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ async def get_by_email(self, session: AsyncSession, email: str) -> User | None:
query = sql.select(User).where(
func.lower(User.email) == email.lower(),
User.deleted_at.is_(None),
User.blocked_at.is_(None),
)
res = await session.execute(query)
return res.scalars().unique().one_or_none()
Expand All @@ -48,6 +49,7 @@ async def get_by_username(
query = sql.select(User).where(
User.username == username,
User.deleted_at.is_(None),
User.blocked_at.is_(None),
)
res = await session.execute(query)
return res.scalars().unique().one_or_none()
Expand All @@ -58,6 +60,7 @@ async def get_by_stripe_customer_id(
query = sql.select(User).where(
User.stripe_customer_id == stripe_customer_id,
User.deleted_at.is_(None),
User.blocked_at.is_(None),
)
res = await session.execute(query)
return res.scalars().unique().one_or_none()
Expand Down
12 changes: 11 additions & 1 deletion server/tests/fixtures/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,13 @@ def __init__(
self,
*,
subject: Literal[
"anonymous", "user", "user_second", "organization", "organization_second"
"anonymous",
"user",
"user_second",
"user_blocked",
"organization",
"organization_second",
"organization_blocked",
] = "user",
scopes: set[Scope] = {Scope.web_default},
method: AuthMethod = AuthMethod.COOKIE,
Expand All @@ -37,8 +43,10 @@ def auth_subject(
request: pytest.FixtureRequest,
user: User,
user_second: User,
user_blocked: User,
organization: Organization,
organization_second: Organization,
organization_blocked: Organization,
) -> AuthSubject[Subject]:
"""
This fixture generates an AuthSubject instance used by the `client` fixture
Expand All @@ -53,8 +61,10 @@ def auth_subject(
"anonymous": Anonymous(),
"user": user,
"user_second": user_second,
"user_blocked": user_blocked,
"organization": organization,
"organization_second": organization_second,
"organization_blocked": organization_blocked,
}
return AuthSubject(
subjects_map[auth_subject_fixture.subject],
Expand Down
46 changes: 46 additions & 0 deletions server/tests/fixtures/random_objects.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,25 @@ async def second_organization(save_fixture: SaveFixture) -> Organization:
return await create_organization(save_fixture)


@pytest_asyncio.fixture(scope="function")
async def organization_blocked(save_fixture: SaveFixture) -> Organization:
organization = Organization(
platform=Platforms.github,
name=rstr("testorg"),
external_id=secrets.randbelow(100000),
avatar_url="https://avatars.githubusercontent.com/u/105373340?s=200&v=4",
is_personal=True,
installation_id=secrets.randbelow(100000),
installation_created_at=datetime.now(),
installation_updated_at=datetime.now(),
installation_suspended_at=None,
created_from_user_maintainer_upgrade=True,
blocked_at=utc_now(),
)
await save_fixture(organization)
return organization


async def create_organization(save_fixture: SaveFixture) -> Organization:
organization = Organization(
platform=Platforms.github,
Expand Down Expand Up @@ -218,6 +237,19 @@ async def user_second(save_fixture: SaveFixture) -> User:
return user


@pytest_asyncio.fixture(scope="function")
async def user_blocked(save_fixture: SaveFixture) -> User:
user = User(
id=uuid.uuid4(),
username=rstr("DEPRECATED_testuser"),
email=rstr("test") + "@example.com",
avatar_url="https://avatars.githubusercontent.com/u/47952?v=4",
blocked_at=utc_now(),
)
await save_fixture(user)
return user


async def create_pledge(
save_fixture: SaveFixture,
organization: Organization,
Expand Down Expand Up @@ -398,6 +430,20 @@ async def user_organization_second(
return user_organization


@pytest_asyncio.fixture(scope="function")
async def user_organization_blocked(
save_fixture: SaveFixture,
organization_blocked: Organization,
user: User,
) -> UserOrganization:
user_organization = UserOrganization(
user_id=user.id,
organization_id=organization_blocked.id,
)
await save_fixture(user_organization)
return user_organization


@pytest_asyncio.fixture
async def open_collective_account(save_fixture: SaveFixture, user: User) -> Account:
account = Account(
Expand Down
83 changes: 83 additions & 0 deletions server/tests/organization/test_endpoints.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,17 @@ async def test_get_organization(
assert org.id == organization.id


@pytest.mark.asyncio
@pytest.mark.http_auto_expunge
@pytest.mark.auth
async def test_get_blocked_organization_404(
organization_blocked: Organization, client: AsyncClient
) -> None:
response = await client.get(f"/api/v1/organizations/{organization_blocked.id}")

assert response.status_code == 404


@pytest.mark.asyncio
@pytest.mark.http_auto_expunge
@pytest.mark.auth
Expand Down Expand Up @@ -129,6 +140,23 @@ async def test_list_organization_member(
assert len(response.json()["items"]) == 0


@pytest.mark.asyncio
@pytest.mark.http_auto_expunge
@pytest.mark.auth
async def test_list_blocked_organization_member(
organization_blocked: Organization,
user_organization_blocked: UserOrganization, # makes User a member of Organization
client: AsyncClient,
) -> None:
response = await client.get("/api/v1/organizations")

assert response.status_code == 200

orgs = response.json()["items"]
for org in orgs:
assert org.id != str(organization_blocked.id)


@pytest.mark.asyncio
@pytest.mark.http_auto_expunge
@pytest.mark.auth
Expand Down Expand Up @@ -189,6 +217,19 @@ async def test_organization_lookup(
assert response.json()["id"] == str(organization.id)


@pytest.mark.asyncio
@pytest.mark.http_auto_expunge
@pytest.mark.auth
async def test_organization_blocked_lookup_404(
organization_blocked: Organization, client: AsyncClient
) -> None:
response = await client.get(
f"/api/v1/organizations/lookup?platform=github&organization_name={organization_blocked.name}"
)

assert response.status_code == 404


@pytest.mark.asyncio
@pytest.mark.http_auto_expunge
@pytest.mark.auth
Expand Down Expand Up @@ -217,6 +258,20 @@ async def test_organization_search_no_matches(
assert response.json()["items"] == []


@pytest.mark.asyncio
@pytest.mark.http_auto_expunge
@pytest.mark.auth
async def test_organization_blocked_search(
organization_blocked: Organization, client: AsyncClient
) -> None:
response = await client.get(
f"/api/v1/organizations/search?platform=github&organization_name={organization_blocked.name}"
)

assert response.status_code == 200
assert response.json()["items"] == []


@pytest.mark.asyncio
@pytest.mark.http_auto_expunge
@pytest.mark.auth
Expand All @@ -235,6 +290,20 @@ async def test_get_organization_deleted(
assert response.status_code == 404


@pytest.mark.asyncio
@pytest.mark.http_auto_expunge
@pytest.mark.auth
async def test_get_organization_blocked_404(
save_fixture: SaveFixture,
organization_blocked: Organization,
user_organization: UserOrganization, # makes User a member of Organization
client: AsyncClient,
) -> None:
response = await client.get(f"/api/v1/organizations/{organization_blocked.id}")

assert response.status_code == 404


@pytest.mark.asyncio
@pytest.mark.http_auto_expunge
@pytest.mark.auth
Expand All @@ -249,6 +318,20 @@ async def test_update_organization_no_admin(
assert response.status_code == 401


@pytest.mark.asyncio
@pytest.mark.http_auto_expunge
@pytest.mark.auth
async def test_update_blocked_organization_no_admin_404(
organization_blocked: Organization, client: AsyncClient
) -> None:
response = await client.patch(
f"/api/v1/organizations/{organization_blocked.id}",
json={"default_upfront_split_to_contributors": 85},
)

assert response.status_code == 404


@pytest.mark.asyncio
@pytest.mark.auth
async def test_update_organization(
Expand Down
40 changes: 40 additions & 0 deletions server/tests/user/test_endpoints.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

from polar.models.account import Account
from polar.models.user import User
from tests.fixtures.auth import AuthSubjectFixture


@pytest.mark.asyncio
Expand All @@ -28,6 +29,14 @@ async def test_get_users_me_no_auth(client: AsyncClient) -> None:
assert response.status_code == 401


@pytest.mark.asyncio
@pytest.mark.http_auto_expunge
@pytest.mark.auth(AuthSubjectFixture(subject="user_blocked"))
async def test_get_users_me_blocked(user_blocked: User, client: AsyncClient) -> None:
response = await client.get("/api/v1/users/me")
assert response.status_code == 401


@pytest.mark.asyncio
@pytest.mark.auth
@pytest.mark.http_auto_expunge
Expand Down Expand Up @@ -68,6 +77,21 @@ async def test_set_preferences_false(client: AsyncClient) -> None:
assert "oauth_accounts" in json


@pytest.mark.asyncio
@pytest.mark.auth(AuthSubjectFixture(subject="user_blocked"))
@pytest.mark.http_auto_expunge
async def test_blocked_user_set_preferences(client: AsyncClient) -> None:
response = await client.put(
"/api/v1/users/me",
json={
"email_newsletters_and_changelogs": False,
"email_promotions_and_events": False,
},
)

assert response.status_code == 401


@pytest.mark.asyncio
@pytest.mark.auth
@pytest.mark.http_auto_expunge
Expand All @@ -85,3 +109,19 @@ async def test_set_account(
json = response.json()

assert json["account_id"] == str(open_collective_account.id)


@pytest.mark.asyncio
@pytest.mark.auth(AuthSubjectFixture(subject="user_blocked"))
@pytest.mark.http_auto_expunge
async def test_blocked_user_set_account(
client: AsyncClient, open_collective_account: Account
) -> None:
response = await client.patch(
"/api/v1/users/me/account",
json={
"account_id": str(open_collective_account.id),
},
)

assert response.status_code == 401

0 comments on commit 748d21a

Please sign in to comment.