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() }}