Repository: fossasia/open-event-server

Issue:
Ensure basic endpoints are accessible without authentication
### Event-related endpoints

- [x] Getting a list of events & details about a single event
- [x] Getting a list of sessions of an event and the details about a single session
- [x] Getting a list of microlocations of an event and the details about a single microlocation
- [x] Getting a list of tracks of an event and the details about a single track
- ~Getting a list of speakers of an event and the details about a single speaker~ [ Edited by Piyush: This need to be done separately since speakers API isn't ready yet ]
- [x] Getting a list of tickets of an event and the details about a single tickets
- [x] Getting call for speakers info of an event
- [x] Getting copyright info of an event


### Other endpoints
- [x] Getting settings (all fields except all client IDs, access tokens, secrets, passwords, email settings). But note, `stripe-publishable-key` is required.
- [x] Getting pages list & detail


Discussion:
I think it requires the permission manager to be completed before this. 
@poush updated the description
@poush what is the progres on this ?
Almost complete. Just need to check some end points. Also, since ~sessions~ speakers API isn't implemented yet so that part is left.
Cool, so we have now `has-PR` label :D
@poush looks like this hasn't been done for event relationships ? 

Eg. like `https://api.eventyay.com/v1/events/1/microlocations`
@poush any updates ?
`sponsors` api still remaining as pointed out by @Princu7 
https://api.eventyay.com/v1/events/69/social-links
https://api.eventyay.com/v1/events/69/session-types
https://api.eventyay.com/v1/events/69/event-type
~https://api.eventyay.com/v1/events/69/discount-codes~
https://api.eventyay.com/v1/events/69/relationships/speakers-call

https://api.eventyay.com/v1/sessions/2823/session-type

This requests also requires authentication. Fix it also.
discount codes will need authentication and accessible only to event admins. We don't want someone just enumerating all the discount codes in the system and using them. 

For discount codes, we'll need two new endpoints with only JWT check.
- [ ] `/v1/events/<id>/discount-codes/<discount-code>`
- [ ] `/v1/discount-codes/<discount-code>`

For the rest of the endpoints, 

- [x] `/v1/events/<id>/discount-codes/*` - accessible only to event admin
- [x] `/v1/discount-codes` - accessible only to super admin
- [ ] `/v1/discount-codes/<id>` - if type=ticket, accessible only to event admin. If type=event, accessible only to super admin

@niranjan94 
aren't these two endpoints are same 
`/v1/events/<id>/discount-codes/<discount-code>`
`/v1/discount-codes/<discount-code>`

Since DiscountCode model is associated with events therefore we can easily check for the permission without the need of event_id in url
@poush there are two different kinds of discount codes sharing the same model. See #3911
@niranjan94 Please see if I'm correct here,

For only super admin we will have routes

DiscountList
=>  /v1/discount-codes [GET]
=> /v1/discount-codes [POST]

DiscountDetail
=> /v1/discount-codes/<id> ( with type=event) [GET, PATCH, DELETE]

For Event Admin and super admin

DiscountList
=> /v1/events/<id>/discount-codes [GET and POST]

Discount Detail
=> /v1/discount-codes/<id> (type=ticket)  [GET, PATCH, DELETE]

For everyone logged in 

DiscountDetail
=> /v1/discount-codes/<discount-code>

Also what I am trying to say in the case of
```
/v1/events/<id>/discount-codes/<discount-code>
/v1/discount-codes/<discount-code>
```
is that both uses < discount-code > for same method which is not allowed in JSON API
> is that both uses < discount-code > for same method which is not allowed in JSON API

They both refer to the same resource right ? 
Yes, to DiscountDetail
So ... what's the confusion here ?
I cannot use < discount-code > in two endpoints pointing to same resource.

```
File "--/open-event-orga-server/ENV/lib/python2.7/site-packages/werkzeug/routing.py", line 202, in parse_rule
    raise ValueError('variable name %r used twice.' % variable)
ValueError: variable name 'discount_code' used twice.
```


Then change the variable name to id 😅  .. Please don't take the endpoint variables I give literally ... It's just for reference
This really did not require such a long discussion 
@niranjan94 Before working on DiscountCodes API permissions as you described #3911 has to be completed. I have partial code ready here and will complete it as soon #3911 get done
Hi. Thanks for implementing the Speakers API :) It seems that the basic endpoints like for getting information regarding the speakers of an event are not working without the access token. I tried a call to this URL: http://open-event-api.herokuapp.com/v1/events/69/speakers
Please see to it. Thanks!!


Resolving patch:
diff --git a/app/api/discount_codes.py b/app/api/discount_codes.py
--- a/app/api/discount_codes.py
+++ b/app/api/discount_codes.py
@@ -5,7 +5,6 @@
 import marshmallow.validate as validate
 
 from app.api.helpers.utilities import dasherize
-from flask_jwt import current_identity as current_user
 from app.api.helpers.permissions import jwt_required
 from flask_rest_jsonapi.exceptions import ObjectNotFound
 from sqlalchemy.orm.exc import NoResultFound
@@ -14,6 +13,7 @@
 from app.models.discount_code import DiscountCode
 from app.api.helpers.exceptions import UnprocessableEntity
 from app.api.helpers.db import safe_query
+from app.api.helpers.permission_manager import has_access
 
 
 class DiscountCodeSchemaTicket(Schema):
@@ -134,7 +134,6 @@ class DiscountCodeList(ResourceList):
     """
     List and Create Discount Code
     """
-
     def query(self, view_kwargs):
         """
         query method for Discount Code List
@@ -142,21 +141,15 @@ def query(self, view_kwargs):
         :return:
         """
         query_ = self.session.query(DiscountCode)
-
-        if view_kwargs.get('event_id') and (current_user.is_coorganizer(view_kwargs.get('event_id'))
-           or current_user.is_organizer(view_kwargs.get('event_id')) or current_user.is_admin):
-            self.schema = DiscountCodeSchemaTicket
+        if view_kwargs.get('event_identifier'):
             event = safe_query(self, Event, 'id', view_kwargs['event_id'], 'event_id')
-            query_ = query_.filter_by(event_id=event.id)
-            self.schema = DiscountCodeSchemaTicket
+            view_kwargs['event_id'] = event.id
 
-        elif view_kwargs.get('event_identifier') and (current_user.is_coorganizer(view_kwargs.get('event_id'))
-           or current_user.is_organizer(view_kwargs.get('event_id')) or current_user.is_admin):
-            event = safe_query(self, Event, 'identifier', view_kwargs['event_identifier'], 'event_identifier')
-            query_ = query_.join(Event).filter(Event.id == event.id)
+        if view_kwargs.get('event_id') and has_access('is_coorganizer', event_id=view_kwargs['event_id']):
             self.schema = DiscountCodeSchemaTicket
+            query_ = query_.filter_by(event_id=view_kwargs['event_id'])
 
-        elif (not view_kwargs.get('event_id')) and current_user.is_admin:
+        elif not view_kwargs.get('event_id') and has_access('is_admin'):
             self.schema = DiscountCodeSchemaEvent
         else:
             raise UnprocessableEntity({'source': ''}, "Please check used_for and endpoint and verify your permission")
@@ -170,41 +163,31 @@ def before_create_object(self, data, view_kwargs):
         :param view_kwargs:
         :return:
         """
-        if view_kwargs.get('event_id') and data['used_for'] == 'ticket'\
-           and (current_user.is_organizer(view_kwargs.get('event_id')) or current_user.is_admin):
-            self.schema = DiscountCodeSchemaTicket
+        if view_kwargs.get('event_identifier'):
             event = safe_query(self, Event, 'id', view_kwargs['event_id'], 'event_id')
-            data['event_id'] = event.id
-            try:
-                self.session.query(DiscountCode).filter_by(event_id=data['event_id']).filter_by(used_for='event').one()
-            except NoResultFound:
-                pass
-            else:
-                raise UnprocessableEntity({'parameter': 'event_id'},
-                                          "Discount Code already exists for the provided Event ID")
+            view_kwargs['event_id'] = event.id
 
-        elif view_kwargs.get('event_identifier') and data['used_for'] == 'ticket'\
-         and (current_user.is_organizer(view_kwargs.get('event_id')) or current_user.is_admin):
+        if view_kwargs.get('event_id') and has_access('is_coorganizer', event_id=view_kwargs['event_id'])\
+           and data['used_for'] == 'ticket':
             self.schema = DiscountCodeSchemaTicket
-            event = safe_query(self, Event, 'identifier', view_kwargs['event_identifier'], 'event_identifier')
+            event = safe_query(self, Event, 'id', view_kwargs['event_id'], 'event_id')
             data['event_id'] = event.id
+
             try:
                 self.session.query(DiscountCode).filter_by(event_id=data['event_id']).filter_by(used_for='event').one()
             except NoResultFound:
                 pass
             else:
-                raise UnprocessableEntity({'parameter': 'event_identifier'},
+                raise UnprocessableEntity({'parameter': 'event_id'},
                                           "Discount Code already exists for the provided Event ID")
 
-        elif (not view_kwargs.get('event_identifier')) or not view_kwargs.get('event_id')\
-         and data['used_for'] == 'ticket':
+        elif not view_kwargs.get('event_id') and data['used_for'] == 'ticket':
             raise UnprocessableEntity({'source': ''}, "Use /v1/events/<int:event_id/discout-codes endpoint")
 
-        elif (not view_kwargs.get('event_identifier') or not view_kwargs.get('event_id'))\
-         and data['used_for'] == 'event' and current_user.is_admin:
+        elif not view_kwargs.get('event_id') and data['used_for'] == 'event' and has_access('is_admin'):
             self.schema = DiscountCodeSchemaEvent
 
-        elif (view_kwargs.get('event_identifier') or view_kwargs.get('event_id')) and data['used_for'] == 'event':
+        elif view_kwargs.get('event_id') and data['used_for'] == 'event':
             raise UnprocessableEntity({'source': ''}, "Use /v1/discount-codes endpoint")
 
         else:
@@ -224,7 +207,6 @@ class DiscountCodeDetail(ResourceDetail):
     """
     Discount Code detail by id
     """
-
     def before_get_object(self, view_kwargs):
         """
         query method for Discount Code detail
@@ -235,7 +217,7 @@ def before_get_object(self, view_kwargs):
             event = safe_query(self, Event, 'identifier', view_kwargs['event_identifier'], 'event_identifier')
             view_kwargs['event_id'] = event.id
 
-        if view_kwargs.get('event_id') and current_user.is_admin:
+        if view_kwargs.get('event_id') and has_access('is_admin'):
             event = safe_query(self, Event, 'id', view_kwargs['event_id'], 'event_id')
             if event.discount_code_id:
                 view_kwargs['id'] = event.discount_code_id
@@ -247,10 +229,10 @@ def before_get_object(self, view_kwargs):
             if not discount:
                 raise ObjectNotFound({'parameter': '{id}'}, "DiscountCode:  not found")
 
-            if discount.used_for == 'ticket' and current_user.is_coorganizer(discount.event_id):
+            if discount.used_for == 'ticket' and has_access('is_coorganizer', event_id=discount.event_id):
                 self.schema = DiscountCodeSchemaTicket
 
-            elif discount.used_for == 'event' and current_user.is_admin:
+            elif discount.used_for == 'event' and has_access('is_admin'):
                 self.schema = DiscountCodeSchemaEvent
             else:
                 raise UnprocessableEntity({'source': ''},
@@ -261,47 +243,39 @@ def before_get_object(self, view_kwargs):
                                       "Please verify your permission. You must be admin to view event\
                                       discount code details")
 
-    def before_update_object(self, obj, data, view_kwargs):
+    def before_update_object(self, discount, data, view_kwargs):
         """
         Method to edit object
+        :param discount:
         :param data:
         :param view_kwargs:
         :return:
         """
-        discount = self.session.query(DiscountCode).filter_by(id=view_kwargs.get('id')).one()
-        if not discount:
-            raise ObjectNotFound({'parameter': '{id}'}, "DiscountCode:  not found")
-
         if 'used_for' in data:
             used_for = data['used_for']
-        elif not 'used_for' in data:
+        else:
             used_for = discount.used_for
 
-        if discount.used_for == 'ticket' and (current_user.is_coorganizer(view_kwargs.get('event_id'))
-           or current_user.is_organizer(view_kwargs.get('event_id') or current_user.is_admin)) and used_for != 'event':
+        if discount.used_for == 'ticket' and has_access('is_coorganizer', event_id=view_kwargs.get('event_id'))\
+           and used_for != 'event':
             self.schema = DiscountCodeSchemaTicket
 
-        elif discount.used_for == 'event' and current_user.is_admin and used_for != 'ticket':
+        elif discount.used_for == 'event' and has_access('is_admin') and used_for != 'ticket':
             self.schema = DiscountCodeSchemaEvent
-
         else:
             raise UnprocessableEntity({'source': ''}, "Please check used_for and endpoint and verify your permission")
 
-    def before_delete_object(self, view_kwargs):
+    def before_delete_object(self, discount, view_kwargs):
         """
         Method for Discount Code delete
+        :param discount:
         :param view_kwargs:
         :return:
         """
-        discount = self.session.query(DiscountCode).filter_by(id=view_kwargs.get('id')).one()
-        if not discount:
-            raise ObjectNotFound({'parameter': '{id}'}, "DiscountCode:  not found")
-
-        if discount.used_for == 'ticket' and (current_user.is_coorganizer(view_kwargs.get('event_id'))
-           or current_user.is_organizer(view_kwargs.get('event_id')) or current_user.is_admin):
+        if discount.used_for == 'ticket' and has_access('is_coorganizer', event_id=view_kwargs['event_id']):
             self.schema = DiscountCodeSchemaTicket
 
-        elif discount.used_for == 'event' and current_user.is_admin:
+        elif discount.used_for == 'event' and has_access('is_admin'):
             self.schema = DiscountCodeSchemaEvent
         else:
             raise UnprocessableEntity({'source': ''}, "Please check used_for and endpoint and verify your permission")
@@ -322,7 +296,8 @@ class DiscountCodeRelationship(ResourceRelationship):
     def before_get(self, args, kwargs):
         """
         Method for get relationship
-        :param view_kwargs:
+        :param args:
+        :param kwargs:
         :return:
         """
         discount = db.session.query(DiscountCode).filter_by(id=kwargs.get('id')).one()
diff --git a/app/api/helpers/permission_manager.py b/app/api/helpers/permission_manager.py
--- a/app/api/helpers/permission_manager.py
+++ b/app/api/helpers/permission_manager.py
@@ -154,6 +154,7 @@ def accessible_role_based_events(view, view_args, view_kwargs, *args, **kwargs):
 
 
 permissions = {
+    'is_super_admin': is_super_admin,
     'is_admin': is_admin,
     'is_organizer': is_organizer,
     'is_coorganizer': is_coorganizer,
@@ -186,6 +187,12 @@ def permission_manager(view, view_args, view_kwargs, *args, **kwargs):
     if request.method not in methods:
         return view(*view_args, **view_kwargs)
 
+    # leave_if checks if we have to bypass this request on the basis of lambda function
+    if 'leave_if' in kwargs:
+        check = kwargs['leave_if']
+        if check(view_kwargs):
+            return view(*view_args, **view_kwargs)
+
     # A check to ensure it is good to go ahead and check permissions
     if 'check' in kwargs:
         check = kwargs['check']
@@ -225,3 +232,11 @@ def permission_manager(view, view_args, view_kwargs, *args, **kwargs):
         return permissions[args[0]](view, view_args, view_kwargs, *args, **kwargs)
     else:
         return ForbiddenError({'source': ''}, 'Access forbidden').respond()
+
+
+def has_access(access_level, **kwargs):
+    if access_level in permissions:
+        auth = permissions[access_level](lambda *a, **b: True, (), {}, (), **kwargs)
+        if type(auth) is bool and auth is True:
+            return True
+    return False


Test patch: