diff --git a/deployment/docker-compose/.env.example b/deployment/docker-compose/.env.example index 1f5ca6a..3490916 100644 --- a/deployment/docker-compose/.env.example +++ b/deployment/docker-compose/.env.example @@ -28,4 +28,5 @@ SEO_SITEMAP_PING_GOOGLE=False JWT_PRIVATE_KEY="" JWT_PUBLIC_JWKS='' DOCS_URL='' -ADMIN_UNIT_CREATE_REQUIRES_ADMIN=False \ No newline at end of file +ADMIN_UNIT_CREATE_REQUIRES_ADMIN=False +API_READ_ANONYM=False \ No newline at end of file diff --git a/deployment/docker-compose/docker-compose.yml b/deployment/docker-compose/docker-compose.yml index 83eaa06..bc611e1 100644 --- a/deployment/docker-compose/docker-compose.yml +++ b/deployment/docker-compose/docker-compose.yml @@ -26,6 +26,7 @@ x-web-env: DOCS_URL: ${DOCS_URL} SITE_NAME: ${SITE_NAME} ADMIN_UNIT_CREATE_REQUIRES_ADMIN: ${ADMIN_UNIT_CREATE_REQUIRES_ADMIN:-False} + API_READ_ANONYM: ${API_READ_ANONYM:-False} x-web: &default-web diff --git a/project/__init__.py b/project/__init__.py index 932f37c..65a526a 100644 --- a/project/__init__.py +++ b/project/__init__.py @@ -52,6 +52,7 @@ app.config["SEO_SITEMAP_PING_GOOGLE"] = getenv_bool("SEO_SITEMAP_PING_GOOGLE", " app.config["GOOGLE_MAPS_API_KEY"] = os.getenv("GOOGLE_MAPS_API_KEY") set_env_to_app(app, "SITE_NAME", "EventCally") app.config["FLASK_DEBUG"] = getenv_bool("FLASK_DEBUG", "False") +app.config["API_READ_ANONYM"] = getenv_bool("API_READ_ANONYM", "False") # if app.config["FLASK_DEBUG"]: # logging.basicConfig(level=logging.DEBUG) @@ -268,6 +269,7 @@ from project.views import ( admin_unit, admin_unit_member, admin_unit_member_invitation, + custom_widget, dump, event, event_date, diff --git a/project/access.py b/project/access.py index 7e566e0..4880a41 100644 --- a/project/access.py +++ b/project/access.py @@ -34,6 +34,7 @@ def owner_access_or_401(user_id): def login_api_user() -> bool: return ( current_token + and current_token.user and login_user(current_token.user) or current_user and current_user.is_authenticated diff --git a/project/api/__init__.py b/project/api/__init__.py index b828351..947e094 100644 --- a/project/api/__init__.py +++ b/project/api/__init__.py @@ -1,4 +1,4 @@ -from apispec import APISpec +from apispec import APISpec, BasePlugin from apispec.exceptions import DuplicateComponentNameError from apispec.ext.marshmallow import MarshmallowPlugin from flask import url_for @@ -115,6 +115,11 @@ class RestApi(Api): data["errors"] = errors +class DocSecurityPlugin(BasePlugin): + def operation_helper(self, path=None, operations=None, **kwargs): + pass + + scope_list = [ "openid", "profile", @@ -138,7 +143,7 @@ app.config.update( "APISPEC_SPEC": APISpec( title="Event calendar API", version="0.1.0", - plugins=[marshmallow_plugin], + plugins=[marshmallow_plugin, DocSecurityPlugin()], openapi_version="2.0", info=dict( description="This API provides endpoints to interact with the event calendar data." @@ -174,16 +179,33 @@ def add_oauth2_scheme_with_transport(insecure: bool): authorizationUrl = url_for("authorize", _external=True, _scheme="https") tokenUrl = url_for("issue_token", _external=True, _scheme="https") - oauth2_scheme = { + scopes = {k: k for _, k in enumerate(scope_list)} + oauth2_authorization_code_scheme = { "type": "oauth2", "authorizationUrl": authorizationUrl, "tokenUrl": tokenUrl, "flow": "accessCode", - "scopes": {k: k for _, k in enumerate(scope_list)}, + "scopes": scopes, } try: - api_docs.spec.components.security_scheme("oauth2", oauth2_scheme) + api_docs.spec.components.security_scheme( + "oauth2AuthCode", oauth2_authorization_code_scheme + ) + except DuplicateComponentNameError: # pragma: no cover + pass + + oauth2_client_credentials_scheme = { + "type": "oauth2", + "tokenUrl": tokenUrl, + "flow": "application", + "scopes": scopes, + } + + try: + api_docs.spec.components.security_scheme( + "oauth2ClientCredentials", oauth2_client_credentials_scheme + ) except DuplicateComponentNameError: # pragma: no cover pass diff --git a/project/api/custom_widget/resources.py b/project/api/custom_widget/resources.py index 22ddb8a..2138c6a 100644 --- a/project/api/custom_widget/resources.py +++ b/project/api/custom_widget/resources.py @@ -16,13 +16,13 @@ from project.models import CustomWidget class CustomWidgetResource(BaseResource): @doc(summary="Get custom widget", tags=["Custom Widgets"]) @marshal_with(CustomWidgetSchema) + @require_api_access() def get(self, id): return CustomWidget.query.get_or_404(id) @doc( summary="Update custom widget", tags=["Custom Widgets"], - security=[{"oauth2": ["customwidget:write"]}], ) @use_kwargs(CustomWidgetPostRequestSchema, location="json", apply=False) @marshal_with(None, 204) @@ -42,7 +42,6 @@ class CustomWidgetResource(BaseResource): @doc( summary="Patch custom widget", tags=["Custom Widgets"], - security=[{"oauth2": ["customwidget:write"]}], ) @use_kwargs(CustomWidgetPatchRequestSchema, location="json", apply=False) @marshal_with(None, 204) @@ -62,7 +61,6 @@ class CustomWidgetResource(BaseResource): @doc( summary="Delete custom widget", tags=["Custom Widgets"], - security=[{"oauth2": ["customwidget:write"]}], ) @marshal_with(None, 204) @require_api_access("customwidget:write") diff --git a/project/api/dump/resources.py b/project/api/dump/resources.py index c98db94..47cfe85 100644 --- a/project/api/dump/resources.py +++ b/project/api/dump/resources.py @@ -2,7 +2,7 @@ from flask_apispec import doc, marshal_with from project.api import add_api_resource from project.api.dump.schemas import DumpResponseSchema -from project.api.resources import BaseResource +from project.api.resources import BaseResource, require_api_access from project.api.schemas import NoneSchema @@ -14,6 +14,7 @@ class DumpResource(BaseResource): ) @marshal_with(NoneSchema, 404) @marshal_with(DumpResponseSchema, 200) + @require_api_access() def get(self, **kwargs): return None, 404 diff --git a/project/api/event/resources.py b/project/api/event/resources.py index 9d86b3f..2943ae4 100644 --- a/project/api/event/resources.py +++ b/project/api/event/resources.py @@ -26,10 +26,9 @@ from project.api.event_date.schemas import ( EventDateListRequestSchema, EventDateListResponseSchema, ) -from project.api.resources import BaseResource +from project.api.resources import BaseResource, require_api_access from project.api.schemas import NoneSchema from project.models import AdminUnit, Event, EventDate, PublicStatus -from project.oauth2 import require_oauth from project.services.event import ( get_event_with_details_or_404, get_events_query, @@ -61,6 +60,7 @@ class EventListResource(BaseResource): @doc(summary="List events", tags=["Events"]) @use_kwargs(EventListRequestSchema, location=("query")) @marshal_with(EventListResponseSchema) + @require_api_access() def get(self, **kwargs): pagination = ( Event.query.join(Event.admin_unit) @@ -78,7 +78,7 @@ class EventListResource(BaseResource): class EventResource(BaseResource): @doc(summary="Get event", tags=["Events"]) @marshal_with(EventSchema) - @require_oauth(optional=True) + @require_api_access() def get(self, id): login_api_user() event = get_event_with_details_or_404(id) @@ -86,11 +86,12 @@ class EventResource(BaseResource): return event @doc( - summary="Update event", tags=["Events"], security=[{"oauth2": ["event:write"]}] + summary="Update event", + tags=["Events"], ) @use_kwargs(EventPostRequestSchema, location="json", apply=False) @marshal_with(None, 204) - @require_oauth("event:write") + @require_api_access("event:write") def put(self, id): login_api_user_or_401() event = Event.query.get_or_404(id) @@ -106,10 +107,13 @@ class EventResource(BaseResource): return make_response("", 204) - @doc(summary="Patch event", tags=["Events"], security=[{"oauth2": ["event:write"]}]) + @doc( + summary="Patch event", + tags=["Events"], + ) @use_kwargs(EventPatchRequestSchema, location="json", apply=False) @marshal_with(None, 204) - @require_oauth("event:write") + @require_api_access("event:write") def patch(self, id): login_api_user_or_401() event = Event.query.get_or_404(id) @@ -126,10 +130,11 @@ class EventResource(BaseResource): return make_response("", 204) @doc( - summary="Delete event", tags=["Events"], security=[{"oauth2": ["event:write"]}] + summary="Delete event", + tags=["Events"], ) @marshal_with(None, 204) - @require_oauth("event:write") + @require_api_access("event:write") def delete(self, id): login_api_user_or_401() event = Event.query.get_or_404(id) @@ -145,7 +150,7 @@ class EventDatesResource(BaseResource): @doc(summary="List dates for event", tags=["Events", "Event Dates"]) @use_kwargs(EventDateListRequestSchema, location=("query")) @marshal_with(EventDateListResponseSchema) - @require_oauth(optional=True) + @require_api_access() def get(self, id, **kwargs): event = Event.query.options( load_only(Event.id, Event.public_status) @@ -164,7 +169,7 @@ class EventSearchResource(BaseResource): @doc(summary="Search for events", tags=["Events"]) @use_kwargs(EventSearchRequestSchema, location=("query")) @marshal_with(EventSearchResponseSchema) - @require_oauth(optional=True) + @require_api_access() def get(self, **kwargs): login_api_user() params = EventSearchParams() @@ -177,6 +182,7 @@ class EventReportsResource(BaseResource): @doc(summary="Add event report", tags=["Events"]) @use_kwargs(EventReportPostSchema, location="json", apply=False) @marshal_with(NoneSchema, 204) + @require_api_access() def post(self, id): event = Event.query.options( load_only(Event.id, Event.public_status) diff --git a/project/api/event_category/resources.py b/project/api/event_category/resources.py index f5e035b..b6cfa37 100644 --- a/project/api/event_category/resources.py +++ b/project/api/event_category/resources.py @@ -5,7 +5,7 @@ from project.api.event_category.schemas import ( EventCategoryListRequestSchema, EventCategoryListResponseSchema, ) -from project.api.resources import BaseResource +from project.api.resources import BaseResource, require_api_access from project.models import EventCategory @@ -13,6 +13,7 @@ class EventCategoryListResource(BaseResource): @doc(summary="List event categories", tags=["Event Categories"]) @use_kwargs(EventCategoryListRequestSchema, location=("query")) @marshal_with(EventCategoryListResponseSchema) + @require_api_access() def get(self, **kwargs): pagination = EventCategory.query.paginate() return pagination diff --git a/project/api/event_date/resources.py b/project/api/event_date/resources.py index a1c7ab1..39f4db8 100644 --- a/project/api/event_date/resources.py +++ b/project/api/event_date/resources.py @@ -12,9 +12,8 @@ from project.api.event_date.schemas import ( EventDateSearchRequestSchema, EventDateSearchResponseSchema, ) -from project.api.resources import BaseResource +from project.api.resources import BaseResource, require_api_access from project.models import AdminUnit, Event, EventDate, PublicStatus -from project.oauth2 import require_oauth from project.services.event import get_event_dates_query from project.services.event_search import EventSearchParams @@ -23,6 +22,7 @@ class EventDateListResource(BaseResource): @doc(summary="List event dates", tags=["Event Dates"]) @use_kwargs(EventDateListRequestSchema, location=("query")) @marshal_with(EventDateListResponseSchema) + @require_api_access() def get(self, **kwargs): pagination = ( EventDate.query.join(EventDate.event) @@ -42,7 +42,7 @@ class EventDateListResource(BaseResource): class EventDateResource(BaseResource): @doc(summary="Get event date", tags=["Event Dates"]) @marshal_with(EventDateSchema) - @require_oauth(optional=True) + @require_api_access() def get(self, id): event_date = EventDate.query.options( defaultload(EventDate.event).load_only( @@ -57,7 +57,7 @@ class EventDateSearchResource(BaseResource): @doc(summary="Search for event dates", tags=["Event Dates"]) @use_kwargs(EventDateSearchRequestSchema, location=("query")) @marshal_with(EventDateSearchResponseSchema) - @require_oauth(optional=True) + @require_api_access() def get(self, **kwargs): login_api_user() params = EventSearchParams() diff --git a/project/api/event_list/resources.py b/project/api/event_list/resources.py index cf05a2a..76d9b45 100644 --- a/project/api/event_list/resources.py +++ b/project/api/event_list/resources.py @@ -19,13 +19,13 @@ from project.services.event_search import EventSearchParams class EventListModelResource(BaseResource): @doc(summary="Get event list", tags=["Event Lists"]) @marshal_with(EventListSchema) + @require_api_access() def get(self, id): return EventList.query.get_or_404(id) @doc( summary="Update event list", tags=["Event Lists"], - security=[{"oauth2": ["eventlist:write"]}], ) @use_kwargs(EventListUpdateRequestSchema, location="json", apply=False) @marshal_with(None, 204) @@ -45,7 +45,6 @@ class EventListModelResource(BaseResource): @doc( summary="Patch event list", tags=["Event Lists"], - security=[{"oauth2": ["eventlist:write"]}], ) @use_kwargs(EventListPatchRequestSchema, location="json", apply=False) @marshal_with(None, 204) @@ -65,7 +64,6 @@ class EventListModelResource(BaseResource): @doc( summary="Delete event list", tags=["Event Lists"], - security=[{"oauth2": ["eventlist:write"]}], ) @marshal_with(None, 204) @require_api_access("eventlist:write") @@ -87,6 +85,7 @@ class EventListEventListResource(BaseResource): ) @use_kwargs(EventListRequestSchema, location=("query")) @marshal_with(EventListResponseSchema) + @require_api_access() def get(self, id, **kwargs): params = EventSearchParams() params.event_list_id = id @@ -98,7 +97,6 @@ class EventListEventListWriteResource(BaseResource): @doc( summary="Add event", tags=["Event Lists", "Events"], - security=[{"oauth2": ["eventlist:write"]}], ) @marshal_with(None, 204) @require_api_access("eventlist:write") @@ -120,7 +118,6 @@ class EventListEventListWriteResource(BaseResource): @doc( summary="Remove event", tags=["Event Lists", "Events"], - security=[{"oauth2": ["eventlist:write"]}], ) @marshal_with(None, 204) @require_api_access("eventlist:write") diff --git a/project/api/event_reference/resources.py b/project/api/event_reference/resources.py index 84531a1..e8c3a6e 100644 --- a/project/api/event_reference/resources.py +++ b/project/api/event_reference/resources.py @@ -2,13 +2,14 @@ from flask_apispec import doc, marshal_with from project.api import add_api_resource from project.api.event_reference.schemas import EventReferenceSchema -from project.api.resources import BaseResource +from project.api.resources import BaseResource, require_api_access from project.models import EventReference class EventReferenceResource(BaseResource): @doc(summary="Get event reference", tags=["Event References"]) @marshal_with(EventReferenceSchema) + @require_api_access() def get(self, id): return EventReference.query.get_or_404(id) diff --git a/project/api/organization/resources.py b/project/api/organization/resources.py index 970d59f..c13b79c 100644 --- a/project/api/organization/resources.py +++ b/project/api/organization/resources.py @@ -74,7 +74,6 @@ from project.api.place.schemas import ( ) from project.api.resources import BaseResource, require_api_access from project.models import AdminUnit, Event, PublicStatus -from project.oauth2 import require_oauth from project.services.admin_unit import ( get_admin_unit_invitation_query, get_admin_unit_query, @@ -98,7 +97,7 @@ from project.views.utils import send_mail class OrganizationResource(BaseResource): @doc(summary="Get organization", tags=["Organizations"]) @marshal_with(OrganizationSchema) - @require_oauth(optional=True) + @require_api_access() def get(self, id): return AdminUnit.query.get_or_404(id) @@ -111,7 +110,7 @@ class OrganizationEventDateSearchResource(BaseResource): ) @use_kwargs(EventDateSearchRequestSchema, location=("query")) @marshal_with(EventDateSearchResponseSchema) - @require_oauth(optional=True) + @require_api_access() def get(self, id, **kwargs): admin_unit = AdminUnit.query.get_or_404(id) @@ -128,7 +127,7 @@ class OrganizationEventSearchResource(BaseResource): @doc(summary="Search for events of organization", tags=["Organizations", "Events"]) @use_kwargs(EventSearchRequestSchema, location=("query")) @marshal_with(EventSearchResponseSchema) - @require_oauth(optional=True) + @require_api_access() def get(self, id, **kwargs): admin_unit = AdminUnit.query.get_or_404(id) @@ -145,7 +144,7 @@ class OrganizationEventListResource(BaseResource): @doc(summary="List events of organization", tags=["Organizations", "Events"]) @use_kwargs(EventListRequestSchema, location=("query")) @marshal_with(EventListResponseSchema) - @require_oauth(optional=True) + @require_api_access() def get(self, id, **kwargs): admin_unit = AdminUnit.query.get_or_404(id) @@ -164,11 +163,10 @@ class OrganizationEventListResource(BaseResource): @doc( summary="Add new event", tags=["Organizations", "Events"], - security=[{"oauth2": ["event:write"]}], ) @use_kwargs(EventPostRequestSchema, location="json", apply=False) @marshal_with(EventIdSchema, 201) - @require_oauth("event:write") + @require_api_access("event:write") def post(self, id): login_api_user_or_401() admin_unit = get_admin_unit_for_manage_or_404(id) @@ -189,6 +187,7 @@ class OrganizationEventImportResource(BaseResource): @marshal_with(EventIdSchema, 201) @require_api_access("event:write") def post(self, id, **kwargs): + login_api_user_or_401() admin_unit = AdminUnit.query.get_or_404(id) access_or_401(admin_unit, "event:create") @@ -211,10 +210,13 @@ class OrganizationEventImportResource(BaseResource): class OrganizationListResource(BaseResource): - @doc(summary="List organizations", tags=["Organizations"]) + @doc( + summary="List organizations", + tags=["Organizations"], + ) @use_kwargs(OrganizationListRequestSchema, location=("query")) @marshal_with(OrganizationListResponseSchema) - @require_oauth(optional=True) + @require_api_access() def get(self, **kwargs): keyword = kwargs["keyword"] if "keyword" in kwargs else None @@ -231,6 +233,7 @@ class OrganizationOrganizerListResource(BaseResource): ) @use_kwargs(OrganizerListRequestSchema, location=("query")) @marshal_with(OrganizerListResponseSchema) + @require_api_access() def get(self, id, **kwargs): admin_unit = AdminUnit.query.get_or_404(id) name = kwargs["name"] if "name" in kwargs else None @@ -241,11 +244,10 @@ class OrganizationOrganizerListResource(BaseResource): @doc( summary="Add new organizer", tags=["Organizations", "Organizers"], - security=[{"oauth2": ["organizer:write"]}], ) @use_kwargs(OrganizerPostRequestSchema, location="json", apply=False) @marshal_with(OrganizerIdSchema, 201) - @require_oauth("organizer:write") + @require_api_access("organizer:write") def post(self, id): login_api_user_or_401() admin_unit = get_admin_unit_for_manage_or_404(id) @@ -264,6 +266,7 @@ class OrganizationPlaceListResource(BaseResource): @doc(summary="List places of organization", tags=["Organizations", "Places"]) @use_kwargs(PlaceListRequestSchema, location=("query")) @marshal_with(PlaceListResponseSchema) + @require_api_access() def get(self, id, **kwargs): admin_unit = AdminUnit.query.get_or_404(id) name = kwargs["name"] if "name" in kwargs else None @@ -274,11 +277,10 @@ class OrganizationPlaceListResource(BaseResource): @doc( summary="Add new place", tags=["Organizations", "Places"], - security=[{"oauth2": ["place:write"]}], ) @use_kwargs(PlacePostRequestSchema, location="json", apply=False) @marshal_with(PlaceIdSchema, 201) - @require_oauth("place:write") + @require_api_access("place:write") def post(self, id): login_api_user_or_401() admin_unit = get_admin_unit_for_manage_or_404(id) @@ -300,6 +302,7 @@ class OrganizationIncomingEventReferenceListResource(BaseResource): ) @use_kwargs(EventReferenceListRequestSchema, location=("query")) @marshal_with(EventReferenceListResponseSchema) + @require_api_access() def get(self, id, **kwargs): admin_unit = AdminUnit.query.get_or_404(id) @@ -314,6 +317,7 @@ class OrganizationOutgoingEventReferenceListResource(BaseResource): ) @use_kwargs(EventReferenceListRequestSchema, location=("query")) @marshal_with(EventReferenceListResponseSchema) + @require_api_access() def get(self, id, **kwargs): admin_unit = AdminUnit.query.get_or_404(id) @@ -325,7 +329,6 @@ class OrganizationOutgoingRelationListResource(BaseResource): @doc( summary="List outgoing relations of organization", tags=["Organizations", "Organization Relations"], - security=[{"oauth2": ["organization:read"]}], ) @use_kwargs(OrganizationRelationListRequestSchema, location=("query")) @marshal_with(OrganizationRelationListResponseSchema) @@ -341,7 +344,6 @@ class OrganizationOutgoingRelationListResource(BaseResource): @doc( summary="Add new outgoing relation", tags=["Organizations", "Organization Relations"], - security=[{"oauth2": ["organization:write"]}], ) @use_kwargs(OrganizationRelationCreateRequestSchema, location="json", apply=False) @marshal_with(OrganizationRelationIdSchema, 201) @@ -364,7 +366,6 @@ class OrganizationOrganizationInvitationListResource(BaseResource): @doc( summary="List organization invitations of organization", tags=["Organizations", "Organization Invitations"], - security=[{"oauth2": ["organization:read"]}], ) @use_kwargs(OrganizationInvitationListRequestSchema, location=("query")) @marshal_with(OrganizationInvitationListResponseSchema) @@ -380,7 +381,6 @@ class OrganizationOrganizationInvitationListResource(BaseResource): @doc( summary="Add new organization invitation", tags=["Organizations", "Organization Invitations"], - security=[{"oauth2": ["organization:write"]}], ) @use_kwargs(OrganizationInvitationCreateRequestSchema, location="json", apply=False) @marshal_with(OrganizationInvitationIdSchema, 201) @@ -413,6 +413,7 @@ class OrganizationEventListListResource(BaseResource): ) @use_kwargs(EventListListRequestSchema, location=("query")) @marshal_with(EventListListResponseSchema) + @require_api_access() def get(self, id, **kwargs): admin_unit = AdminUnit.query.get_or_404(id) name = kwargs["name"] if "name" in kwargs else None @@ -423,7 +424,6 @@ class OrganizationEventListListResource(BaseResource): @doc( summary="Add new event list", tags=["Organizations", "Event Lists"], - security=[{"oauth2": ["eventlist:write"]}], ) @use_kwargs(EventListCreateRequestSchema, location="json", apply=False) @marshal_with(EventListIdSchema, 201) @@ -449,6 +449,7 @@ class OrganizationEventListStatusListResource(BaseResource): ) @use_kwargs(EventListListRequestSchema, location=("query")) @marshal_with(EventListStatusListResponseSchema) + @require_api_access() def get(self, id, event_id, **kwargs): admin_unit = AdminUnit.query.get_or_404(id) name = kwargs["name"] if "name" in kwargs else None @@ -466,6 +467,7 @@ class OrganizationCustomWidgetListResource(BaseResource): ) @use_kwargs(CustomWidgetListRequestSchema, location=("query")) @marshal_with(CustomWidgetListResponseSchema) + @require_api_access() def get(self, id, **kwargs): admin_unit = AdminUnit.query.get_or_404(id) name = kwargs["name"] if "name" in kwargs else None @@ -476,7 +478,6 @@ class OrganizationCustomWidgetListResource(BaseResource): @doc( summary="Add new custom widget", tags=["Organizations", "CustomWidgets"], - security=[{"oauth2": ["customwidget:write"]}], ) @use_kwargs(CustomWidgetPostRequestSchema, location="json", apply=False) @marshal_with(CustomWidgetIdSchema, 201) diff --git a/project/api/organization_invitation/resources.py b/project/api/organization_invitation/resources.py index d7fe511..613a4d8 100644 --- a/project/api/organization_invitation/resources.py +++ b/project/api/organization_invitation/resources.py @@ -18,7 +18,6 @@ class OrganizationInvitationResource(BaseResource): @doc( summary="Get organization invitation", tags=["Organization Invitations"], - security=[{"oauth2": ["organization:read"]}], ) @marshal_with(OrganizationInvitationSchema) @require_api_access("organization:read") @@ -32,7 +31,6 @@ class OrganizationInvitationResource(BaseResource): @doc( summary="Update organization invitation", tags=["Organization Invitations"], - security=[{"oauth2": ["organization:write"]}], ) @use_kwargs(OrganizationInvitationUpdateRequestSchema, location="json", apply=False) @marshal_with(None, 204) @@ -52,7 +50,6 @@ class OrganizationInvitationResource(BaseResource): @doc( summary="Patch organization invitation", tags=["Organization Invitations"], - security=[{"oauth2": ["organization:write"]}], ) @use_kwargs(OrganizationInvitationPatchRequestSchema, location="json", apply=False) @marshal_with(None, 204) @@ -72,7 +69,6 @@ class OrganizationInvitationResource(BaseResource): @doc( summary="Delete organization invitation", tags=["Organization Invitations"], - security=[{"oauth2": ["organization:write"]}], ) @marshal_with(None, 204) @require_api_access("organization:write") diff --git a/project/api/organization_relation/resources.py b/project/api/organization_relation/resources.py index 76c30b4..cee3267 100644 --- a/project/api/organization_relation/resources.py +++ b/project/api/organization_relation/resources.py @@ -19,7 +19,6 @@ class OrganizationRelationResource(BaseResource): @doc( summary="Get organization relation", tags=["Organization Relations"], - security=[{"oauth2": ["organization:read"]}], ) @marshal_with(OrganizationRelationSchema) @require_api_access("organization:read") @@ -37,7 +36,6 @@ class OrganizationRelationResource(BaseResource): @doc( summary="Update organization relation", tags=["Organization Relations"], - security=[{"oauth2": ["organization:write"]}], ) @use_kwargs(OrganizationRelationUpdateRequestSchema, location="json", apply=False) @marshal_with(None, 204) @@ -57,7 +55,6 @@ class OrganizationRelationResource(BaseResource): @doc( summary="Patch organization relation", tags=["Organization Relations"], - security=[{"oauth2": ["organization:write"]}], ) @use_kwargs(OrganizationRelationPatchRequestSchema, location="json", apply=False) @marshal_with(None, 204) @@ -77,7 +74,6 @@ class OrganizationRelationResource(BaseResource): @doc( summary="Delete organization relation", tags=["Organization Relations"], - security=[{"oauth2": ["organization:write"]}], ) @marshal_with(None, 204) @require_api_access("organization:write") diff --git a/project/api/organizer/resources.py b/project/api/organizer/resources.py index d2ed6b3..0f63ee2 100644 --- a/project/api/organizer/resources.py +++ b/project/api/organizer/resources.py @@ -9,25 +9,24 @@ from project.api.organizer.schemas import ( OrganizerPostRequestSchema, OrganizerSchema, ) -from project.api.resources import BaseResource +from project.api.resources import BaseResource, require_api_access from project.models import EventOrganizer -from project.oauth2 import require_oauth class OrganizerResource(BaseResource): @doc(summary="Get organizer", tags=["Organizers"]) @marshal_with(OrganizerSchema) + @require_api_access() def get(self, id): return EventOrganizer.query.get_or_404(id) @doc( summary="Update organizer", tags=["Organizers"], - security=[{"oauth2": ["organizer:write"]}], ) @use_kwargs(OrganizerPostRequestSchema, location="json", apply=False) @marshal_with(None, 204) - @require_oauth("organizer:write") + @require_api_access("organizer:write") def put(self, id): login_api_user_or_401() organizer = EventOrganizer.query.get_or_404(id) @@ -41,11 +40,10 @@ class OrganizerResource(BaseResource): @doc( summary="Patch organizer", tags=["Organizers"], - security=[{"oauth2": ["organizer:write"]}], ) @use_kwargs(OrganizerPatchRequestSchema, location="json", apply=False) @marshal_with(None, 204) - @require_oauth("organizer:write") + @require_api_access("organizer:write") def patch(self, id): login_api_user_or_401() organizer = EventOrganizer.query.get_or_404(id) @@ -61,10 +59,9 @@ class OrganizerResource(BaseResource): @doc( summary="Delete organizer", tags=["Organizers"], - security=[{"oauth2": ["organizer:write"]}], ) @marshal_with(None, 204) - @require_oauth("organizer:write") + @require_api_access("organizer:write") def delete(self, id): login_api_user_or_401() organizer = EventOrganizer.query.get_or_404(id) diff --git a/project/api/place/resources.py b/project/api/place/resources.py index d488348..346d151 100644 --- a/project/api/place/resources.py +++ b/project/api/place/resources.py @@ -9,23 +9,24 @@ from project.api.place.schemas import ( PlacePostRequestSchema, PlaceSchema, ) -from project.api.resources import BaseResource +from project.api.resources import BaseResource, require_api_access from project.models import EventPlace -from project.oauth2 import require_oauth class PlaceResource(BaseResource): @doc(summary="Get place", tags=["Places"]) @marshal_with(PlaceSchema) + @require_api_access() def get(self, id): return EventPlace.query.get_or_404(id) @doc( - summary="Update place", tags=["Places"], security=[{"oauth2": ["place:write"]}] + summary="Update place", + tags=["Places"], ) @use_kwargs(PlacePostRequestSchema, location="json", apply=False) @marshal_with(None, 204) - @require_oauth("place:write") + @require_api_access("place:write") def put(self, id): login_api_user_or_401() place = EventPlace.query.get_or_404(id) @@ -36,10 +37,13 @@ class PlaceResource(BaseResource): return make_response("", 204) - @doc(summary="Patch place", tags=["Places"], security=[{"oauth2": ["place:write"]}]) + @doc( + summary="Patch place", + tags=["Places"], + ) @use_kwargs(PlacePatchRequestSchema, location="json", apply=False) @marshal_with(None, 204) - @require_oauth("place:write") + @require_api_access("place:write") def patch(self, id): login_api_user_or_401() place = EventPlace.query.get_or_404(id) @@ -51,10 +55,11 @@ class PlaceResource(BaseResource): return make_response("", 204) @doc( - summary="Delete place", tags=["Places"], security=[{"oauth2": ["place:write"]}] + summary="Delete place", + tags=["Places"], ) @marshal_with(None, 204) - @require_oauth("place:write") + @require_api_access("place:write") def delete(self, id): login_api_user_or_401() place = EventPlace.query.get_or_404(id) diff --git a/project/api/resources.py b/project/api/resources.py index 273f381..5ea2947 100644 --- a/project/api/resources.py +++ b/project/api/resources.py @@ -1,13 +1,13 @@ from functools import wraps from authlib.oauth2 import OAuth2Error -from authlib.oauth2.rfc6749 import MissingAuthorizationError from flask import request from flask_apispec import marshal_with +from flask_apispec.annotations import annotate from flask_apispec.views import MethodResource -from flask_security import current_user +from flask_wtf.csrf import validate_csrf -from project import db +from project import app, csrf, db from project.api.schemas import ErrorResponseSchema, UnprocessableEntityResponseSchema from project.oauth2 import require_oauth @@ -22,23 +22,44 @@ def etag_cache(func): return wrapper -def require_api_access(scopes=None, optional=False): +def is_internal_request() -> bool: + try: + validate_csrf(csrf._get_csrf_token()) + return True + except Exception: + return False + + +def require_api_access(scopes=None): def inner_decorator(func): def wrapped(*args, **kwargs): # see authlib ResourceProtector#__call__ try: # pragma: no cover try: require_oauth.acquire_token(scopes) - except MissingAuthorizationError as error: - if optional: - return func(*args, **kwargs) - require_oauth.raise_error_response(error) except OAuth2Error as error: require_oauth.raise_error_response(error) except Exception as e: - if not current_user or not current_user.is_authenticated: + if app.config["API_READ_ANONYM"]: + return func(*args, **kwargs) + if not is_internal_request(): raise e return func(*args, **kwargs) + scope_list = scopes if type(scopes) is list else [scopes] if scopes else list() + security = [{"oauth2AuthCode": scope_list}] + + if not scope_list: + security.append( + { + "oauth2ClientCredentials": scope_list, + } + ) + + annotate( + wrapped, + "docs", + [{"security": security}], + ) return wrapped return inner_decorator diff --git a/project/api/user/resources.py b/project/api/user/resources.py index 8b66088..f11b6f5 100644 --- a/project/api/user/resources.py +++ b/project/api/user/resources.py @@ -34,7 +34,6 @@ class UserOrganizationInvitationListResource(BaseResource): @doc( summary="List organization invitations of user", tags=["Users", "Organization Invitations"], - security=[{"oauth2": ["user:read"]}], ) @use_kwargs(OrganizationInvitationListRequestSchema, location=("query")) @marshal_with(OrganizationInvitationListResponseSchema) @@ -53,7 +52,6 @@ class UserOrganizationInvitationResource(BaseResource): @doc( summary="Get organization invitation of user", tags=["Users", "Organization Invitations"], - security=[{"oauth2": ["user:read"]}], ) @marshal_with(OrganizationInvitationSchema) @require_api_access("user:read") @@ -67,7 +65,6 @@ class UserOrganizationInvitationResource(BaseResource): @doc( summary="Delete organization invitation of user", tags=["Users", "Organization Invitations"], - security=[{"oauth2": ["user:write"]}], ) @marshal_with(None, 204) @require_api_access("user:write") @@ -86,7 +83,6 @@ class UserFavoriteEventListResource(BaseResource): @doc( summary="List favorite events of user", tags=["Users", "Events"], - security=[{"oauth2": ["user:read"]}], ) @use_kwargs(UserFavoriteEventListRequestSchema, location=("query")) @marshal_with(UserFavoriteEventListResponseSchema) @@ -104,7 +100,6 @@ class UserFavoriteEventSearchResource(BaseResource): @doc( summary="Search for favorite events of user", tags=["Users", "Events"], - security=[{"oauth2": ["user:read"]}], ) @use_kwargs(EventSearchRequestSchema, location=("query")) @marshal_with(EventSearchResponseSchema) @@ -124,7 +119,6 @@ class UserFavoriteEventListWriteResource(BaseResource): @doc( summary="Add event to users favorites", tags=["Users", "Events"], - security=[{"oauth2": ["user:write"]}], ) @marshal_with(None, 204) @require_api_access("user:write") @@ -142,7 +136,6 @@ class UserFavoriteEventListWriteResource(BaseResource): @doc( summary="Remove event from users favorites", tags=["Users", "Events"], - security=[{"oauth2": ["user:write"]}], ) @marshal_with(None, 204) @require_api_access("user:write") diff --git a/project/cli/test.py b/project/cli/test.py index ff0048e..2a128e0 100644 --- a/project/cli/test.py +++ b/project/cli/test.py @@ -258,9 +258,6 @@ def _insert_default_oauth2_client(user_id): metadata = dict() metadata["client_name"] = "Mein Client" metadata["scope"] = " ".join(scope_list) - metadata["grant_types"] = ["authorization_code", "refresh_token"] - metadata["response_types"] = ["code"] - metadata["token_endpoint_auth_method"] = "client_secret_post" metadata["redirect_uris"] = ["/"] client.set_client_metadata(metadata) diff --git a/project/forms/oauth2_client.py b/project/forms/oauth2_client.py index 1c87099..66265bc 100644 --- a/project/forms/oauth2_client.py +++ b/project/forms/oauth2_client.py @@ -3,7 +3,7 @@ import os from flask_babel import lazy_gettext from flask_wtf import FlaskForm from wtforms import StringField, SubmitField, TextAreaField -from wtforms.validators import DataRequired +from wtforms.validators import DataRequired, Optional from project.api import scopes from project.forms.widgets import MultiCheckboxField @@ -13,11 +13,11 @@ from project.utils import split_by_crlf class BaseOAuth2ClientForm(FlaskForm): client_name = StringField(lazy_gettext("Client name"), validators=[DataRequired()]) redirect_uris = TextAreaField( - lazy_gettext("Redirect URIs"), validators=[DataRequired()] + lazy_gettext("Redirect URIs"), validators=[Optional()] ) scope = MultiCheckboxField( lazy_gettext("Scopes"), - validators=[DataRequired()], + validators=[Optional()], choices=[(k, k) for k, v in scopes.items()], ) diff --git a/project/init_data.py b/project/init_data.py index 4361ee3..2134b47 100644 --- a/project/init_data.py +++ b/project/init_data.py @@ -47,16 +47,7 @@ def create_initial_data(): "reference_request:delete", "reference_request:verify", ] - early_adopter_permissions = [ - "oauth2_client:create", - "oauth2_client:read", - "oauth2_client:update", - "oauth2_client:delete", - "oauth2_token:create", - "oauth2_token:read", - "oauth2_token:update", - "oauth2_token:delete", - ] + early_adopter_permissions = [] upsert_admin_unit_member_role("admin", "Administrator", admin_permissions) upsert_admin_unit_member_role("event_verifier", "Event expert", event_permissions) diff --git a/project/models/admin_unit.py b/project/models/admin_unit.py index 518caa1..0f3f6af 100644 --- a/project/models/admin_unit.py +++ b/project/models/admin_unit.py @@ -276,7 +276,7 @@ class AdminUnit(db.Model, TrackableMixin): server_default="0", ) ) - incoming_verification_requests_text = Column(UnicodeText()) + incoming_verification_requests_text = deferred(Column(UnicodeText())) can_invite_other = deferred( Column( Boolean(), diff --git a/project/models/oauth.py b/project/models/oauth.py index 4a9d868..561c3fe 100644 --- a/project/models/oauth.py +++ b/project/models/oauth.py @@ -22,7 +22,7 @@ class OAuth2Client(db.Model, OAuth2ClientMixin): @OAuth2ClientMixin.grant_types.getter def grant_types(self): - return ["authorization_code", "refresh_token"] + return ["authorization_code", "refresh_token", "client_credentials"] @OAuth2ClientMixin.response_types.getter def response_types(self): diff --git a/project/oauth2.py b/project/oauth2.py index 5980e42..5b0cbdd 100644 --- a/project/oauth2.py +++ b/project/oauth2.py @@ -71,6 +71,10 @@ class AuthorizationCodeGrant(grants.AuthorizationCodeGrant): return db.session.get(User, authorization_code.user_id) +class ClientCredentialsGrant(grants.ClientCredentialsGrant): + TOKEN_ENDPOINT_AUTH_METHODS = ["client_secret_basic", "client_secret_post"] + + class OpenIDCode(_OpenIDCode): def exists_nonce(self, nonce, request): return exists_nonce(nonce, request) @@ -176,6 +180,7 @@ def config_oauth(app): AuthorizationCodeGrant, [CodeChallenge(required=True), OpenIDCode()], ) + authorization.register_grant(ClientCredentialsGrant) authorization.register_grant(RefreshTokenGrant) # support revocation diff --git a/project/requests.py b/project/requests.py index d1f72d0..feb59a3 100644 --- a/project/requests.py +++ b/project/requests.py @@ -44,6 +44,8 @@ def set_manage_admin_unit_cookie(response): @app.after_request def set_response_headers(response): if request and request.endpoint: + if request.endpoint.startswith("api_"): + return response if request.endpoint != "static" and request.endpoint != "widget_event_dates": response.headers["X-Frame-Options"] = "SAMEORIGIN" diff --git a/project/static/vue/widget-configurator/configurator.vue.js b/project/static/vue/widget-configurator/configurator.vue.js index b9e57c1..a245266 100644 --- a/project/static/vue/widget-configurator/configurator.vue.js +++ b/project/static/vue/widget-configurator/configurator.vue.js @@ -411,7 +411,7 @@ const WidgetConfigurator = { }), computed: { iFrameSource() { - return `${window.location.origin}/static/widget/${this.widgetType}.html`; + return `${window.location.origin}/custom_widget/${this.widgetType}`; }, organizationId() { return this.$route.params.organization_id; diff --git a/project/static/widget-loader.js b/project/static/widget-loader.js index 6ee1330..3728ca0 100644 --- a/project/static/widget-loader.js +++ b/project/static/widget-loader.js @@ -38,11 +38,11 @@ var googleTagManager = false; if (customId != null) { - var url = baseUrl + "/api/v1/custom-widgets/" + customId; + var url = baseUrl + "/js/wlcw/" + customId; customWidgetData = loadJSON(url); var settings = customWidgetData.settings; - src = baseUrl + "/static/widget/" + customWidgetData.widget_type + ".html"; + src = baseUrl + "/custom_widget/" + customWidgetData.widget_type; if (settings.hasOwnProperty('iFrameAutoResize') && settings.iFrameAutoResize != null) { resize = settings.iFrameAutoResize; @@ -233,6 +233,8 @@ xmlhttp.overrideMimeType(mimeType); } + xhr.setRequestHeader("X-CSRFToken", "{{ csrf_token() }}"); + xmlhttp.send(); return xmlhttp.responseText; } diff --git a/project/templates/_macros.html b/project/templates/_macros.html index b68a918..f226a6d 100644 --- a/project/templates/_macros.html +++ b/project/templates/_macros.html @@ -1687,24 +1687,6 @@ $('#allday').on('change', function() { {% endmacro %} -{% macro render_ajax_csrf_script() %} - -{% endmacro %} - -{% macro render_ajax_csrf() %} - var csrf_token = "{{ csrf_token() }}"; - - $.ajaxSetup({ - beforeSend: function(xhr, settings) { - if (!/^(GET|HEAD|OPTIONS|TRACE)$/i.test(settings.type) && !this.crossDomain) { - xhr.setRequestHeader("X-CSRFToken", csrf_token); - } - } - }); -{% endmacro %} - {% macro render_form_scripts() %} @@ -1712,8 +1694,6 @@ $('#allday').on('change', function() { + {% block header %} {% endblock %} {%- endblock head %} diff --git a/project/templates/layout_vue.html b/project/templates/layout_vue.html index 11f8f93..49d367d 100644 --- a/project/templates/layout_vue.html +++ b/project/templates/layout_vue.html @@ -331,6 +331,7 @@ }); axios.defaults.baseURL = "{{ get_base_url() }}"; + axios.defaults.headers.common["X-CSRFToken"] = "{{ csrf_token() }}"; axios.interceptors.request.use( function (config) { if (config) { diff --git a/project/templates/manage/export.html b/project/templates/manage/export.html index 29a0e9a..e840375 100644 --- a/project/templates/manage/export.html +++ b/project/templates/manage/export.html @@ -1,10 +1,8 @@ {% extends "layout.html" %} -{% from "_macros.html" import render_ajax_csrf_script %} {%- block title -%} {{ _('Export') }} {%- endblock -%} {% block header %} -{{ render_ajax_csrf_script() }}