Skip to content

Commit

Permalink
Merge pull request #46 from 5-Bits-in-a-Byte/roles-backend-setup
Browse files Browse the repository at this point in the history
Roles backend setup
  • Loading branch information
sampeters747 authored Apr 29, 2021
2 parents ad8b0da + 7203cb7 commit 8f02d4d
Show file tree
Hide file tree
Showing 11 changed files with 341 additions and 68 deletions.
10 changes: 6 additions & 4 deletions server/README-server.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,10 +43,11 @@ This component handles setting up all of our MongoDB models so that each collect
"courses": [
{
"courseId": "oasif-jo12j-asdjf-asdf9",
"courseName": "course name",

"courseName" : "Test course",
"color": "#ffffff",
"nickname": null,
"role": "roleId" // NEW (04.18.21)
"role": "id of the role" // NEW (04.18.21)
}
]
}
Expand Down Expand Up @@ -160,7 +161,8 @@ This component handles setting up all of our MongoDB models so that each collect
"course": "CIS 210",
"canJoinById": true,
"instructorID": "oasif-jo12j-asdjf-asdf9",
"roles": ["roleId1", "roleId2"] // NEW (04.18.21)
"roles": ["roleId1", "roleId2"], // NEW (04.18.21)
"defaultRole": "roleId1"
}
```

Expand All @@ -169,7 +171,7 @@ This component handles setting up all of our MongoDB models so that each collect
```json
{
"_id": "role id goes here",
"roleName": "the name of the role goes here",
"name": "the name of the role goes here",
"permissions": {
"publish": {
"postComment": true,
Expand Down
19 changes: 12 additions & 7 deletions server/inquire/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,28 +74,29 @@ def after_request(response):
api_base_url='https://api.github.com/',
client_kwargs={'scope': 'read:user user:email'},
)
from inquire.resources import Demo, Me, Courses, Posts, Comments, Replies, Join, Reactions, Home
from inquire.resources import Demo, Me, Courses, Posts, Comments, Replies, Join, Reactions, Home, Roles

api = Api(app, prefix="/api")

# Adding Swagger API docs to app
swagger = Swagger(app, config=config.swagger_config)

# register endpoints from /resources folder here:
api.add_resource(Roles, '/courses/<string:courseId>/roles')
api.add_resource(Demo, '/demo')
api.add_resource(Me, '/me')
api.add_resource(Courses, '/courses')
api.add_resource(Reactions, '/reactions')
api.add_resource(Reactions, '/courses/<string:courseId>/reactions')
api.add_resource(Home, '/home')
api.add_resource(Posts, '/courses/<string:courseId>/posts')
api.add_resource(
Comments, '/posts/<string:postId>/comments')
Comments, '/courses/<string:courseId>/posts/<string:postId>/comments')
api.add_resource(
Replies, '/posts/<string:postId>/comments/<string:comment_id>/replies')
Replies, 'courses/<string:courseId>/posts/<string:postId>/comments/<string:comment_id>/replies')
api.add_resource(Join, '/join')
if include_socketio:
# Wrapping flask app in socketio wrapper
from socketio_app import io
from inquire.socketio_app import io
io.init_app(app)
app.socketio = io
return io, app
Expand All @@ -104,5 +105,9 @@ def after_request(response):


if __name__ == '__main__':
io, app = create_app()
io.run(app, host="0.0.0.0", debug=False, log_output=True)
if False:
io, app = create_app()
io.run(app, host="0.0.0.0", debug=True, log_output=True)
else:
app = create_app(include_socketio=False)
app.run(host="0.0.0.0", debug=True)
101 changes: 65 additions & 36 deletions server/inquire/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,14 @@ def get_current_user():
payload = decode_jwt(cookie)
_id = payload['_id']
user = retrieve_user(_id)
user.permissions = None
if user and 'courseId' in request.view_args:
courseId = request.view_args['courseId']
user_course = user.get_course(courseId)
if user_course:
role = retrieve_role(user_course.role)
if role:
user.permissions = role.permissions
g.current_user = user

return g.current_user
Expand All @@ -74,15 +82,42 @@ def teardown_current_user(exception):
user = g.pop('current_user', None)


def permission_layer(required_permissions: list, require_login=True):
def _deep_access(d, keys):
for key in keys:
try:
d = d[key]
except:
return None
return d

def _permission_comparison(user_permissions: dict, required_permissions: list):
"""
Checks if the values in the user_permissions dict are true for the keys specified in the required_permissions list.
For nested values specify keys in the following format "level1key-level2key-level3key".
Required_permissions can contain a subset of permissions to check.
Example: _permission_comparison({"a":True, b:False, c:{d:True, e:True}}, ["a", "c-e"]) == True
"""
Checks if the current_user has the correct permissions to access the endpoint
for the given course
missing_permissions = []
for key in required_permissions:
val = _deep_access(user_permissions, key.split("-"))
if val == None:
app.logger.error(f"Tried to check nonexistant permission: {key}")
missing_permissions.append(key)
elif val == False:
missing_permissions.append(key)
elif val == True:
pass
else:
app.logger.error(f"Non-bool val stored in role permission dict")

Args:
required_permissions (list): List of permissions required to access the endpoint
require_login (bool, optional): [description]. If the user is required
to be logged in. Defaults to True.
return missing_permissions


def permission_layer(required_permissions: list, require_login=True):
"""
Retricts access to an endpoint based on the user's permissions in the course
"""
def actual_decorator(func):
@wraps(func)
Expand All @@ -91,33 +126,14 @@ def wrapper(*args, **kwargs):
if current_user == None and (required_permissions or require_login):
abort(401, errors=[
"Resource access restricted: unauthenticated client"])

errors = []
# Getting the current course
courseId = kwargs.get('courseId')
if courseId or required_permissions:
course = current_user.get_course(courseId)
if course is None:
errors.append(
"Resource access restricted: invalid course id")

# Checking if the user has the required course specific permissions
if required_permissions and course:
missing = []
for permission in required_permissions:
user_perm = getattr(
course, permission, False)
if not user_perm:
missing.append(permission)
if missing:
errors.append(
f'Resource access restricted: missing course permission(s) {", ".join(missing)}')
# If there are any errors, we return a 403
if errors:
return abort(403, errors=errors)
# If there are no errors, we return the result of executing the resource function
else:
return func(*args, **kwargs)
if required_permissions:
if not current_user.permissions:
missing_permissions = required_permissions
else:
missing_permissions = _permission_comparison(current_user.permissions, required_permissions)
if missing_permissions:
abort(401, errors=[f'Resource access restricted: missing course permission(s) {", ".join(missing_permissions)}'])
return func(*args, **kwargs)
return wrapper
return actual_decorator

Expand Down Expand Up @@ -194,17 +210,30 @@ def decode_jwt(s):


def retrieve_user(_id):
'''Retrieves the user from the database using the google "sub" field as _id'''
'''Retrieves the user from the database using the _id field'''
query = User.objects.raw({'_id': _id})
count = query.count()
if count > 1:
raise Exception(
f'Duplicate user detected, multiple users in database with id {sub}')
f'Duplicate user detected, multiple users in database with id {_id}')
elif count == 1:
return query.first()
else:
return None

def retrieve_role(_id):
'''Retrieves the role object from the database'''
query = Role.objects.raw({'_id': ObjectId(_id)})
count = query.count()
if count > 1:
raise Exception(
f'Duplicate roles detected, multiple roles in database with id {_id}')
elif count == 1:
return query.first()
else:
return None



def create_user(data, mode="google"):
if mode == "google":
Expand Down
48 changes: 38 additions & 10 deletions server/inquire/mongo.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,19 @@
Last Modified Date: 03/12/2021
'''
from inquire.config import MONGO_URI

from inquire.roles import student
from pymodm import MongoModel, fields, EmbeddedMongoModel
from pymongo.write_concern import WriteConcern
from pymodm.connection import connect
from pymodm.errors import ValidationError
import pymongo
import json
import shortuuid
import datetime
from bson.objectid import ObjectId

import os
import sys
script_dir = os.path.dirname(__file__)

class User(MongoModel):
_id = fields.CharField(primary_key=True)
Expand All @@ -29,6 +32,7 @@ class User(MongoModel):
courses = fields.EmbeddedDocumentListField(
'UserCourse', blank=True, required=True)


def get_course(self, courseId):
for course in self.courses:
if course.courseId == courseId:
Expand All @@ -44,17 +48,11 @@ class Meta:


class UserCourse(EmbeddedMongoModel):
courseId = fields.CharField()
courseId = fields.CharField(required=True)
courseName = fields.CharField(required=True)
nickname = fields.CharField(blank=True)
color = fields.CharField(blank=True)
canPost = fields.BooleanField(default=True)
seePrivate = fields.BooleanField(default=False)
canPin = fields.BooleanField(default=False)
canRemove = fields.BooleanField(default=False)
canEndorse = fields.BooleanField(default=False)
viewAnonymous = fields.BooleanField(default=False)
admin = fields.BooleanField(default=False)
role = fields.CharField(required=True)


'''
Expand Down Expand Up @@ -118,10 +116,40 @@ class Course(MongoModel):
course = fields.CharField()
canJoinById = fields.BooleanField()
instructorID = fields.CharField()
roles = fields.ListField(required=True)
defaultRole = fields.ObjectIdField(required=True)
_id = fields.CharField(primary_key=True, default=shortuuid.uuid)

class Meta:
write_concern = WriteConcern(j=True)
connection_alias = 'my-app'

indexes = [pymongo.IndexModel([('$**', pymongo.TEXT)])]

def role_validator(d, example=None):
"""For use with the Roles Model"""
# Loop through each field
# if field exists:
# role_validator(field, example_field)
if example == None:
example = student
for key, item in example.items():
if key in d:
if type(item) == dict and type(d[key]) == dict:
if not role_validator(d[key], example=item):
raise ValidationError("Missing key")
elif type(item) != bool or type(d[key]) != bool:
raise ValidationError(f"Wrong type in permission field: {key}")
else:
raise ValidationError(f"Missing permission field: {key}")

return True

class Role(MongoModel):
name = fields.CharField(required=True)
permissions = fields.DictField(default=False, validators=[role_validator])
class Meta:
write_concern = WriteConcern(j=True)
connection_alias = 'my-app'


1 change: 1 addition & 0 deletions server/inquire/resources/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@
from .posts import Posts
from .reactions import Reactions
from .replies import Replies
from .roles import Roles
1 change: 1 addition & 0 deletions server/inquire/resources/comments.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ def get(self, postId=None):

return [self.serialize(comment) for comment in Comment.objects.raw({'postId': postId})]

@permission_layer(required_permissions=["publish-postComment"])
def post(self, postId=None):
"""
Creates a new comment
Expand Down
23 changes: 12 additions & 11 deletions server/inquire/resources/courses.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
from inquire.mongo import *
from inquire.auth import permission_layer, current_user
from inquire.config import DEFAULT_COLORS

from inquire.roles import student, admin
# Courses
# POST - Handles course creation
# Access - Instructor Only
Expand Down Expand Up @@ -77,21 +77,22 @@ def post(self):
color = pick_color(DEFAULT_COLORS)
else:
color = args['color']
print(color, args['color'])
# Creating initial roles available in course
student_role = Role(name="student", permissions=student).save()
admin_role = Role(name="admin", permissions=admin).save()
roles = [student_role._id, admin_role._id]

# Add the course to the user's course list and create the course
course = Course(course=args.course,
canJoinById=args.canJoinById, instructorID=current_user._id).save()
canJoinById=args.canJoinById, instructorID=current_user._id, roles=roles, defaultRole=student_role._id).save()

# Appends the course with permissions to the user who created it
User.objects.raw({'_id': current_user._id}).update({"$push": {"courses":
{"courseId": course._id, "courseName": args.course,
"canPost": True, "seePrivate": True, "canPin": True,
"canRemove": True, "canEndorse": True, "viewAnonymous": True,
"admin": True, "color": color}}})
user_course = UserCourse(courseId=course._id, courseName=args.course, color=color, role=admin_role._id)
current_user.courses.append(user_course)
current_user.save()

return {"courseId": course._id, "courseName": args.course,
"canPost": True, "seePrivate": True, "canPin": True,
"can_remove": True, "canEndorse": True, "viewAnonymous": True,
"admin": True, "color": color}, 200
"color": color}, 200

def delete(self):
# Parse argument
Expand Down
Loading

0 comments on commit 8f02d4d

Please sign in to comment.