def test_delete_group(self, client, fake_auth, mock_category): """Teacher can delete category.""" teacher = User.find_by_canvas_user_id(9876543) fake_auth.login(teacher.id) assert client.delete( f'/api/category/{mock_category.id}/delete').status_code == 200 assert client.get( f'/api/category/{mock_category.id}').status_code == 404
def mock_asset(app, db_session): course = Course.create( canvas_api_domain='bcourses.berkeley.edu', canvas_course_id=randrange(1000000), ) category_hidden = Category.create( canvas_assignment_name='Just look into her false colored eyes', course_id=course.id, title='What a clown (visible=False)', canvas_assignment_id=98765, visible=False, ) category_visible = Category.create( canvas_assignment_name='Just look into her false colored eyes', course_id=course.id, title='What a clown (visible=True)', canvas_assignment_id=98765, visible=True, ) course = Course.query.order_by(Course.name).all()[0] canvas_user_id = str(randint(1000000, 9999999)) user = User.create( canvas_course_role='Student', canvas_course_sections=[], canvas_email=f'{canvas_user_id}@berkeley.edu', canvas_enrollment_state='active', canvas_full_name=f'Student {canvas_user_id}', canvas_user_id=canvas_user_id, course_id=course.id, ) unique_token = datetime.now().isoformat() asset = Asset.create( asset_type='link', categories=[category_hidden, category_visible], course_id=course.id, description=None, download_url= f"s3://{app.config['AWS_S3_BUCKET_FOR_ASSETS']}/asset/{course.id}_{canvas_user_id}_{unique_token}.pdf", title=f'Mock Asset created at {unique_token}', url=f'https://en.wikipedia.org/wiki/{unique_token}', users=[user], ) for test_comment in _get_mock_comments(): comment = Comment.create(asset=asset, body=test_comment['body'], user_id=user.id) for reply in test_comment.get('replies', []): reply = Comment.create( asset=asset, body=reply['body'], parent_id=comment.id, user_id=user.id, ) std_commit(allow_test_environment=True) yield asset db_session.delete(asset) std_commit(allow_test_environment=True)
def test_unauthorized(self, client, fake_auth, mock_category): """Denies unauthorized user.""" student = User.find_by_canvas_user_id(8765432) fake_auth.login(student.id) self._api_update_category( client, category_id=mock_category.id, expected_status_code=401, title='I don\'t do too much talking', )
def _decorate_comments(comments): user_ids = [] for comment in comments: user_ids.append(comment['userId']) for reply in comment.get('replies', []): user_ids.append(reply['userId']) users_by_id = {user.id: user for user in User.find_by_ids(user_ids)} for comment in comments: comment['user'] = users_by_id[comment['userId']].to_api_json() for reply in comment.get('replies', []): reply['user'] = users_by_id[reply['userId']].to_api_json() return comments
def _verify_update_category(self, client, fake_auth, mock_category): teacher = User.find_by_canvas_user_id(9876543) fake_auth.login(teacher.id) title = "I've stopped my rambling, I don't do too much gambling these days" visible = not mock_category.visible api_json = self._api_update_category(client, category_id=mock_category.id, title=title, visible=visible) assert api_json['id'] == mock_category.id assert api_json['title'] == mock_category.title assert api_json['visible'] is visible
def _create_users(courses): course = courses[0] users = [] for test_user in _test_users: user = User.create( canvas_course_role=test_user['canvas_course_role'], canvas_course_sections=[], canvas_email=test_user['canvas_email'], canvas_enrollment_state=test_user['canvas_enrollment_state'], canvas_full_name=test_user['canvas_full_name'], canvas_user_id=test_user['canvas_user_id'], course_id=course.id, ) users.append(user) std_commit(allow_test_environment=True) return users
def test_unauthorized(self, client): """No cookie auth for unauthorized user.""" canvas_course_id = 1502871 unauthorized_user = User.find_by_canvas_user_id(canvas_user_id=654321) assert unauthorized_user assert unauthorized_user.course.canvas_course_id != canvas_course_id canvas_api_domain = unauthorized_user.course.canvas_api_domain client.set_cookie('localhost', f'{canvas_api_domain}|{canvas_course_id}', str(unauthorized_user.id)) self._api_assets_with_cookie_auth( canvas_api_domain=canvas_api_domain, canvas_course_id=canvas_course_id, client=client, expected_status_code=401, )
def test_like_asset_by_non_course_user(self, client, fake_auth, mock_asset): """A user in a different course can't like an asset.""" second_course = Course.create( canvas_api_domain='bcourses.berkeley.edu', canvas_course_id=randrange(1000000), ) second_course_user = User.create( canvas_course_role='Student', canvas_enrollment_state='active', canvas_full_name='Doug Yule', canvas_user_id=randrange(1000000), course_id=second_course.id, ) fake_auth.login(second_course_user.id) self._api_like_asset(asset_id=mock_asset.id, client=client, expected_status_code=404)
def create_asset(): params = request.get_json() or request.form asset_type = params.get('type') category_id = params.get('categoryId') description = params.get('description') source = params.get('source') url = params.get('url') title = params.get('title', url) visible = params.get('visible', True) if not asset_type or not title: raise BadRequestError('Asset creation requires title and type.') if asset_type == 'link' and not url: raise BadRequestError('Link asset creation requires url.') if not current_user.course: raise BadRequestError('Course data not found') s3_attrs = {} if asset_type == 'file': file_upload = _get_upload_from_http_post() s3_attrs = Asset.upload_to_s3( filename=file_upload['name'], byte_stream=file_upload['byte_stream'], course_id=current_user.course.id, ) asset = Asset.create( asset_type=asset_type, categories=category_id and [Category.find_by_id(category_id)], course_id=current_user.course.id, description=description, download_url=s3_attrs.get('download_url', None), mime=s3_attrs.get('content_type', None), source=source, title=title, url=url, users=[User.find_by_id(current_user.get_id())], visible=visible, ) return tolerant_jsonify(asset.to_api_json())
def test_authorized(self, client): """Cookie auth for authorized user.""" canvas_course_id = 1502870 authorized_user = User.find_by_canvas_user_id(canvas_user_id=654321) assert authorized_user assert authorized_user.course.canvas_course_id == canvas_course_id canvas_api_domain = authorized_user.course.canvas_api_domain client.set_cookie('localhost', f'{canvas_api_domain}|{canvas_course_id}', str(authorized_user.id)) self._api_assets_with_cookie_auth( canvas_api_domain=canvas_api_domain, canvas_course_id=canvas_course_id, client=client, ) # Finally, log out and verify that cookie has been removed. assert client.post('/api/auth/logout').status_code == 200 self._api_assets_with_cookie_auth( canvas_api_domain=canvas_api_domain, canvas_course_id=canvas_course_id, client=client, expected_status_code=401, )
def _create_user_at_lti_launch(): _response = self._api_auth_lti_launch( client=client, custom_canvas_api_domain=canvas.canvas_api_domain, custom_canvas_course_id=canvas_course_id, custom_canvas_user_id=canvas_user_id, custom_external_tool_url=external_tool_url, lis_person_name_full=full_name, oauth_consumer_key=canvas.lti_key, roles='Student', tool_id=TOOL_ID_ASSET_LIBRARY, ) std_commit(allow_test_environment=True) user = User.find_by_canvas_user_id(canvas_user_id) assert user assert user.canvas_full_name == full_name assert user.canvas_user_id == canvas_user_id course = user.course assert course.canvas_course_id == canvas_course_id assert course.engagement_index_url is None assert course.asset_library_url == external_tool_url return _response
def test_unauthorized(self, client, fake_auth): """Denies unauthorized user.""" student = User.find_by_canvas_user_id(8765432) fake_auth.login(student.id) self._api_create_category(client, expected_status_code=401)
def _lti_launch_authentication(tool_id): is_asset_library = tool_id == TOOL_ID_ASSET_LIBRARY is_engagement_index = tool_id == TOOL_ID_ENGAGEMENT_INDEX if not is_asset_library and not is_engagement_index: raise BadRequestError(f'Missing or invalid tool_id: {tool_id}') def _alpha_num(s): value = _str_strip(s) return value if value.isalnum() else None args = request.form lti_params = {} validation = { 'custom_canvas_api_domain': _str_strip, 'custom_canvas_course_id': _str_strip, 'custom_canvas_user_id': _str_strip, 'custom_external_tool_url': _canvas_external_tool_url, 'lis_person_name_full': _str_strip, 'oauth_consumer_key': _alpha_num, 'oauth_nonce': _alpha_num, 'oauth_signature_method': _str_strip, 'oauth_timestamp': _str_strip, 'oauth_version': _str_strip, 'roles': _str_strip, } def _fetch(key): value = args.get(key) validate = validation[key] pass_headers = validate is _canvas_external_tool_url validated_value = validate(value, request.headers) if pass_headers else validate(value) if validated_value: lti_params[key] = validated_value return lti_params[key] else: app.logger.warning(f'Invalid \'{key}\' parameter in LTI launch: {value}') if all(_fetch(key) for key in validation.keys()): app.logger.info(f'LTI launch params passed basic validation: {lti_params}') canvas_api_domain = lti_params['custom_canvas_api_domain'] canvas = Canvas.find_by_domain(canvas_api_domain) if not canvas: raise ResourceNotFoundError(f'Failed \'canvas\' lookup where canvas_api_domain = {canvas_api_domain}') if canvas.lti_key != lti_params['oauth_consumer_key']: raise BadRequestError(f'oauth_consumer_key does not match {canvas_api_domain} lti_key in squiggy db.') tool_provider = FlaskToolProvider.from_flask_request( request=request, secret=canvas.lti_secret, ) valid_request = tool_provider.is_valid_request(LtiRequestValidator(canvas)) if valid_request or app.config['TESTING']: # TODO: We do not want app.config['TESTING'] in this conditional. It is here because our tests are failing # on HMAC-signature verification. Let's fix after we get a successful LTI launch. app.logger.info(f'FlaskToolProvider validated {canvas_api_domain} LTI launch request.') else: raise BadRequestError(f'LTI oauth failed in {canvas_api_domain} request') external_tool_url = lti_params['custom_external_tool_url'] canvas_course_id = lti_params['custom_canvas_course_id'] course = Course.find_by_canvas_course_id( canvas_api_domain=canvas_api_domain, canvas_course_id=canvas_course_id, ) if course: course = Course.update( asset_library_url=external_tool_url if is_asset_library else course.asset_library_url, course_id=course.id, engagement_index_url=external_tool_url if is_engagement_index else course.engagement_index_url, ) app.logger.info(f'Updated course during LTI launch: {course.to_api_json()}') else: course = Course.create( asset_library_url=external_tool_url if is_asset_library else None, canvas_api_domain=canvas_api_domain, canvas_course_id=canvas_course_id, engagement_index_url=external_tool_url if is_engagement_index else None, name=args.get('context_title'), ) app.logger.info(f'Created course via LTI launch: {course.to_api_json()}') canvas_user_id = lti_params['custom_canvas_user_id'] user = User.find_by_course_id(canvas_user_id=canvas_user_id, course_id=course.id) if user: app.logger.info(f'Found user during LTI launch: canvas_user_id={canvas_user_id}, course_id={course.id}') else: user = User.create( course_id=course.id, canvas_user_id=canvas_user_id, canvas_course_role=str(lti_params['roles']), canvas_enrollment_state=args.get('custom_canvas_enrollment_state') or 'active', canvas_full_name=lti_params['lis_person_name_full'], canvas_image=args.get('user_image'), # TODO: Verify user_image. canvas_email=args.get('lis_person_contact_email_primary'), canvas_course_sections=None, # TODO: Set by poller? ) app.logger.info(f'Created user during LTI launch: canvas_user_id={canvas_user_id}') path = '/assets' if is_asset_library else '/engage' params = f'canvasApiDomain={canvas_api_domain}&canvasCourseId={canvas_course_id}' app.logger.info(f'LTI launch redirect: {path}?{params}') return user, f'{path}?{params}'
def test_admin(self, client, fake_auth, authorized_user_id): """Cookies for authenticated user.""" user = User.find_by_id(authorized_user_id) fake_auth.login(user.id) self._assert_cookie(client=client, user=user)
def test_load_admin_user(self, authorized_user_id): """Returns authorization record to Flask-Login for recognized UID.""" loaded_user = User.find_by_id(authorized_user_id) assert loaded_user.id == authorized_user_id
def test_load_unknown_user(self): """Returns None to Flask-Login for unrecognized UID.""" assert User.find_by_id(unauthorized_user_id) is None
def __init__(self, user_id): self.user = User.find_by_id(user_id) if user_id else None
def get_users(): return tolerant_jsonify([ u.to_api_json() for u in User.get_users_by_course_id(course_id=current_user.course.id) ])
def test_unauthorized(self, client, fake_auth, mock_category): """Denies unauthorized user.""" student = User.find_by_canvas_user_id(8765432) fake_auth.login(student.id) assert client.delete( f'/api/category/{mock_category.id}/delete').status_code == 401
def poll_users(self, db_course, api_course): # noqa C901 db_users_by_canvas_id = {u.canvas_user_id: u for u in db_course.users} api_sections = list(api_course.get_sections(include=['students'])) app.logger.info(f'Retrieved {len(api_sections)} sections from Canvas: {_format_course(db_course)}') api_sections_by_user_id = {} for s in api_sections: for u in (s.students or []): user_id = u['id'] if api_sections_by_user_id.get(user_id): api_sections_by_user_id[user_id].append(s.name) else: api_sections_by_user_id[user_id] = [s.name] api_users = list(api_course.get_users(include=['enrollments', 'avatar_url', 'email'])) app.logger.info(f'Retrieved {len(api_users)} users from Canvas: {_format_course(db_course)}') api_user_ids = set() for u in api_users: api_user_ids.add(u.id) enrollment_state = 'active' course_role = 'Student' enrollment = next((e for e in u.enrollments if e['course_id'] == db_course.canvas_course_id), None) if not enrollment: enrollment_state = 'completed' else: if enrollment['enrollment_state'] in ['active', 'completed', 'inactive', 'invited', 'rejected']: enrollment_state = enrollment['enrollment_state'] if enrollment['role'] in ['Adv Designer', 'DesignerEnrollment', 'TaEnrollment', 'TeacherEnrollment']: course_role = 'urn:lti:role:ims/lis/Instructor' user_attributes = { 'canvas_course_role': course_role, 'canvas_enrollment_state': enrollment_state, 'canvas_full_name': u.name, 'canvas_user_id': u.id, 'course_id': db_course.id, 'canvas_course_sections': api_sections_by_user_id.get(u.id, []), 'canvas_email': getattr(u, 'email', None), 'canvas_image': getattr(u, 'avatar_url', None), } db_user = db_users_by_canvas_id.get(u.id) if not db_user: app.logger.debug(f'Adding new user {u.id}: {_format_course(db_course)}') db_users_by_canvas_id[u.id] = User.create(**user_attributes) else: updated = False for key, value in user_attributes.items(): if getattr(db_user, key, None) != value: setattr(db_user, key, value) updated = True if updated: app.logger.debug(f'Updating info for user {db_user.canvas_user_id}: {_format_course(db_course)}') db.session.add(db_user) std_commit() for db_user in db_users_by_canvas_id.values(): if db_user.canvas_user_id not in api_user_ids and db_user.canvas_enrollment_state != 'inactive': app.logger.debug(f'Marking user {db_user.canvas_user_id} as inactive: {_format_course(db_course)}') db_user.canvas_enrollment_state = 'inactive' db.session.add(db_user) std_commit() return db_users_by_canvas_id