Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WIP: GraphQL API (public parts) #888

Open
wants to merge 50 commits into
base: main
Choose a base branch
from
Open
Changes from 3 commits
Commits
Show all changes
50 commits
Select commit Hold shift + click to select a range
ec6270d
graphql: Some initial work
Kurocon Apr 17, 2023
feb7f3e
Merge branch 747-single-sign-on
Kurocon Apr 17, 2023
96eed3e
graphql: Add all public models to GraphQL for the members module
supertom01 Apr 17, 2023
6b6e103
Merge branch 'main' into 741-graphql-api
Kurocon Jun 15, 2023
36ebbae
graphql: Add schemas for videos, publications, update schema for news…
Kurocon Jun 16, 2023
2ff19c4
graphql: Re-implement members graphql to only include public models a…
Kurocon Jun 16, 2023
908ab1f
graphql: Add queries for Education pages
Kurocon Jun 18, 2023
496972a
Merge remote-tracking branch 'origin/main' into 741-graphql-api
Oct 2, 2023
6267bd9
Add a basic activities query
supertom01 Oct 16, 2023
df88044
Merge branch 'main' into 741-graphql-api
Kurocon Oct 23, 2023
88cdaed
Extend activity graphql endpoint
supertom01 Oct 30, 2023
c5cd1b0
Finalize public details for activities on GraphQL API
supertom01 Nov 13, 2023
8321e13
Merge pull request #817 from Inter-Actief/811-add-the-activities-modu…
supertom01 Nov 13, 2023
9fe5a27
Add descriptions to attributes and their translations
supertom01 Nov 13, 2023
4b24469
Merge origin/741-graph
supertom01 Nov 13, 2023
8e5262d
Merge pull request #818 from Inter-Actief/811-add-the-activities-modu…
supertom01 Nov 13, 2023
4070ecf
Add about pages to the GraphQL API
supertom01 Dec 11, 2023
5f12f07
Add translations
supertom01 Dec 11, 2023
09134c8
Add the company module to the GraphQL API
supertom01 Dec 11, 2023
9ae9d61
Fix identation
supertom01 Dec 11, 2023
5ebfc39
Add translations
supertom01 Dec 11, 2023
bbe0053
Merge pull request #826 from Inter-Actief/companies-graphql
Mihai98924 Feb 5, 2024
edd5448
Merge origin/741 into 822
supertom01 Feb 5, 2024
402b5ec
Merge pull request #824 from Inter-Actief/822-add-the-about-module-to…
Mihai98924 Feb 5, 2024
be20cc0
WIP
supertom01 Mar 18, 2024
2d2d637
Add a query for a single activity given its id
supertom01 Jun 3, 2024
dd5d945
Merge branch 'main'
Kurocon Jun 3, 2024
fc1b23e
Merge branch 'main' into 741-graphql-api
Kurocon Jun 3, 2024
782061d
Add the calendar modules to all the different Event inheriters
supertom01 Jun 10, 2024
93b7157
Add translations
supertom01 Jun 10, 2024
c941e82
Add mutation for the educational bouquet
supertom01 Jun 10, 2024
358b157
Fix merge conflicts
supertom01 Sep 2, 2024
b57476b
Merge pull request #870 from Inter-Actief/825-add-the-calendar-module…
supertom01 Sep 2, 2024
9cebceb
Merge branch '741-graphql-api' into 871-mutation-graphql-educational-…
supertom01 Sep 2, 2024
9e5cff0
Merge pull request #872 from Inter-Actief/871-mutation-graphql-educat…
supertom01 Sep 2, 2024
c25739f
Merge branch 'main' into 741-graphql-api
Kurocon Sep 2, 2024
104ead3
graphql: Fix invalid fields showing up for Event Types, remove some p…
Kurocon Sep 7, 2024
6bb9700
graphql: Fix non-public attachments and photos, inactive banners, and…
Kurocon Sep 16, 2024
686bc58
graphql: Initial framework for testing private models and fields
Kurocon Sep 30, 2024
c618510
Merge branch 'main'
Kurocon Oct 14, 2024
d90e2aa
graphql: Add more tests for activities, add tests for companyEvents
Kurocon Oct 14, 2024
3494332
graphql: Add tests for education and files modules
Kurocon Oct 28, 2024
70bf329
graphql: Add tests for members module, update tests for activities, e…
Kurocon Nov 1, 2024
966174d
Merge branch 'main' into 741-graphql-api
Kurocon Dec 9, 2024
908852a
graphql: Add tests for news, publications and videos modules.
Kurocon Dec 9, 2024
df3ed6c
Implement authentication on GraphQL types, on a field level using dec…
supertom01 Dec 9, 2024
a75777a
graphql: Small changes after review
Kurocon Dec 23, 2024
eb56e45
Merge branch 'main' into 741-graphql-api
Kurocon Dec 23, 2024
f683427
Extend documentation and add a list of exempt fields
supertom01 Dec 23, 2024
f971d7b
Merge pull request #916 from Inter-Actief/846-work-on-graphql-authent…
Kurocon Dec 24, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
99 changes: 99 additions & 0 deletions amelie/graphql/decorators.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
from graphql import GraphQLError

from graphql_jwt.decorators import user_passes_test, login_required
from graphql_jwt.exceptions import PermissionDenied


def _get_attribute(obj, dotted_path):
value = obj
@@ -24,3 +27,99 @@ def wrapper_args_allow_only_self_or_board(self, info, *args, **kwargs):
return func(self, info, *args, **kwargs)
return wrapper_args_allow_only_self_or_board
return wrapper_allow_only_self_or_board


AUTHORIZATION_FIELD_TYPES = ["public_fields", "login_fields", "committee_fields", "board_fields", "private_fields"]

def is_board_or_www(user):
is_board = hasattr(user, 'person') and hasattr(user.person, 'is_board') and user.person.is_board
is_superuser = hasattr(user, 'is_superuser') and user.is_superuser
return is_board or is_superuser

def committee_required(committees: list):
return user_passes_test(lambda u:is_board_or_www(u) or (hasattr(u, 'person') and hasattr(u.person, 'is_in_committee') and any(u.person.is_in_committee(committee) for committee in committees)))

def board_required():
return user_passes_test(lambda u: is_board_or_www(u))

def no_access():
return user_passes_test(lambda u: False)

def check_authorization(cls):
"""
Enforces authorization checks when this model is queried.

There are multiple types of fields, each can be defined on the DjangoObjectType:

* public_fields: Fields that are accessible for people within being signed in.
* login_fields: Fields that are only accessible after being signed in.
* committee_fields: Fields that are only accessible by members of a committee, WWW superusers, and board members.
* allowed_committees: When committee fields are defined, acronyms of visible committees should be passed.
* board_fields: Fields that are only accessible by WWW superusers and board members.
* private_fields: Fields that cannot be queried through the GraphQL API.
* exempt_fields: Fields that are exempt from these checks,
their resolvers should have their own authorization checking.

An example class would be:
```python
class FooType(DjangoObjectType):
public_fields = ['id']
login_fields = ['login']
committee_fields = ['committee']
allowed_committees = ['some-committee']
board_fields = ['board']
private_fields = ['private']
exempt_fields = ['exempt']

class Meta:
model = Foo
fields = ['id', 'login', 'committee', 'board', 'private', 'exempt']

def resolve_exempt(obj: Foo, info):
# Custom authorization checks
return obj
```
"""
# Make sure that at least one of the authorization fields is present.
if not any(hasattr(cls, authorization_field) for authorization_field in AUTHORIZATION_FIELD_TYPES):
raise ValueError(f"At least one authorization field type should be defined for a GraphQL type, choose from: {', '.join(AUTHORIZATION_FIELD_TYPES)}")

public_fields = getattr(cls, "public_fields", [])
login_fields = getattr(cls, "login_fields", [])
committee_fields = getattr(cls, "committee_fields", [])
board_fields = getattr(cls, "board_fields", [])
private_fields = getattr(cls, "private_fields", [])
exempt_fields = getattr(cls, "exempt_fields", [])

allowed_committees = getattr(cls, "allowed_committees", [])

# If there are committee fields defined, then the allowed committee list cannot be non-empty
if len(committee_fields) > 0 and len(allowed_committees) == 0:
raise ValueError(f"The following fields are only visible by a committee: \"{','.join(committee_fields)}\", but there are no committees defined that can view this field. Make sure that \"allowed_committees\" has at least a single entry.")

# Make sure that all the fields in the authorization fields are mutually exclusive.
authorization_fields = [*public_fields, *login_fields, *committee_fields, *board_fields, *private_fields, *exempt_fields]
if len(authorization_fields) != len(set(authorization_fields)):
raise ValueError("Some of the authorization fields have overlapping Django fields. Make sure that they are all mutually exclusive!")

# Make sure that all the fields that are defined in the fields list are in the authorization fields.
if not all((missing_field := field) in authorization_fields for field in cls._meta.fields):
raise ValueError(f"The field \"{missing_field}\" is defined in the Django fields list, but not in an authorization field list. All the django fields must be present in the authorization fields.")

# Require a user to be signed in.
for login_field in login_fields:
setattr(cls, f"resolve_{login_field}", login_required(lambda self, info, field=login_field: getattr(self, login_field)))

# Require a user to be in a committee
for committee_field in committee_fields:
setattr(cls, f"resolve_{committee_field}", committee_required(allowed_committees)(lambda self, info, field=committee_field: getattr(self, committee_field)))

# Require a user to be in the board
for board_field in board_fields:
setattr(cls, f"resolve_{board_field}", board_required()(lambda self, info, field=board_field: getattr(self, board_field)))

# No-one can access these fields
for private_field in private_fields:
setattr(cls, f"resolve_{private_field}", no_access()(lambda self, info, field: False))

return cls
23 changes: 23 additions & 0 deletions amelie/members/graphql.py
Original file line number Diff line number Diff line change
@@ -6,6 +6,8 @@
from django_filters import FilterSet
from django.db.models import Q
from django.utils.translation import gettext_lazy as _

from amelie.graphql.decorators import check_authorization
from amelie.graphql.pagination.connection_field import DjangoPaginationConnectionField
from amelie.members.models import Committee, Function, CommitteeCategory

@@ -68,7 +70,28 @@ def include_abolished_filter(self, qs, filter_field, value):
return qs.filter(abolished__isnull=True)


@check_authorization
class CommitteeType(DjangoObjectType):
public_fields = [
"id",
"name",
"category",
"parent_committees",
"slug",
"email",
"abolished",
"website",
"information_nl",
"information_en",
"group_picture",
"function_set"
]
committee_fields = [
"founded"
]
allowed_committees = ["WWW"]
private_fields = ["logo", "information"]

class Meta:
model = Committee
description = "Type definition for a single Committee"
Loading