Пример #1
0
def self_information() -> t.Union[JSONResponse[models.User],
                                  JSONResponse[t.Dict[int, str]],
                                  ExtendedJSONResponse[models.User],
                                  ]:
    """Get the info of the currently logged in user.

    .. :quickref: User; Get information about the currently logged in user.

    :query type: If this is ``roles`` a mapping between course_id and role name
        will be returned, if this is ``extended`` the result of
        :py:meth:`.models.User.__extended_to_json__()` will be returned. If
        this is something else or not present the result of
        :py:meth:`.models.User.__to_json__()` will be returned.
    :query with_permissions: Setting this to true will add the key
        ``permissions`` to the user. The value will be a mapping indicating
        which global permissions this user has.
    :returns: A response containing the JSON serialized user

    :raises PermissionException: If there is no logged in user. (NOT_LOGGED_IN)
    """
    args = request.args
    if args.get('type') == 'roles':
        return JSONResponse.make(
            {
                role.course_id: role.name
                for role in current_user.courses.values()
            }
        )
    elif helpers.extended_requested() or args.get('type') == 'extended':
        user = models.User.resolve(current_user)
        if request_arg_true('with_permissions'):
            jsonify_options.get_options().add_permissions_to_user = user
        return ExtendedJSONResponse.make(user, use_extended=models.User)

    return JSONResponse.make(current_user)
Пример #2
0
    def as_task(self, fun: t.Callable[[], None]) -> None:
        """Run the given ``fun`` as the task.

        .. warning::

            One of the first things this function do is committing the current
            session, however after running ``fun`` nothing is committed.

        :param fun: The function to run as the task, catching the exceptions it
            produces and storing them in this task result.
        :returns: Nothing.
        """
        assert self.state == TaskResultState.not_started, (
            'Cannot start task that has already started, state was in {}'
        ).format(self.state)

        self.state = TaskResultState.started
        db.session.commit()

        try:
            self.result = fun()
        except APIException as exc:
            self.state = TaskResultState.failed
            self.result = JSONResponse.dump_to_object(exc)
        except:  # pylint: disable=bare-except
            logger.warning('The task crashed', exc_info=True)
            self.state = TaskResultState.crashed
            self.result = JSONResponse.dump_to_object(
                APIException(
                    'The task failed for an unknown reason',
                    f'The task {self.id} failed with a uncaught exception',
                    APICodes.UNKOWN_ERROR, 400))
        else:
            self.state = TaskResultState.finished
Пример #3
0
def test_jsonify():
    class Enum(CGEnum):
        name_a = 2
        name_b = 3

    with flask.Flask(__name__).app_context():
        assert JSONResponse.dump_to_object(Enum.name_a) == 'name_a'
        assert JSONResponse.dump_to_object(Enum.name_b) == 'name_b'
Пример #4
0
    def as_task(
        self,
        fun: t.Callable[[], t.Optional[TaskResultState]],
        *,
        eta: t.Optional[DatetimeWithTimezone] = None,
    ) -> bool:
        """Run the given ``fun`` as the task.

        .. warning::

            One of the first things this function do is committing the current
            session, however after running ``fun`` nothing is committed.

        :param fun: The function to run as the task, catching the exceptions it
            produces and storing them in this task result.
        :param eta: The time the task should run, if the current time is before
            this ETA the current celery task will be scheduled again and
            ``fun`` will not be called.

        :returns: ``True`` if the task ran, otherwise ``False``.
        """
        if not self.state.is_not_started:  # pragma: no cover
            logger.error('Cannot start task that has already started',
                         task_result=self)
            return False
        elif eta and current_task.maybe_delay_task(eta):
            return False

        self.state = TaskResultState.started
        db.session.commit()

        try:
            result_code = fun()
        except APIException as exc:
            self.state = TaskResultState.failed
            self.result = JSONResponse.dump_to_object(exc)
        except:  # pylint: disable=bare-except
            logger.warning('The task crashed', exc_info=True)
            self.state = TaskResultState.crashed
            self.result = JSONResponse.dump_to_object(
                APIException(
                    'The task failed for an unknown reason',
                    f'The task {self.id} failed with a uncaught exception',
                    APICodes.UNKOWN_ERROR, 400))
        else:
            self.result = None
            self.state = handle_none(result_code, TaskResultState.finished)

        return True
Пример #5
0
def update_site_settings(
) -> t.Union[JSONResponse[site_settings.Opt.AllOptsAsJSON],
             JSONResponse[site_settings.Opt.FrontendOptsAsJSON],
             ]:
    """Get the settings for this CodeGrade instance.

    .. :quickref: Site settings; Get the site settings for this instance.
    """
    options = rqa.FixedMapping(
        rqa.RequiredArgument(
            'updates',
            rqa.List(site_settings.OPTIONS_INPUT_PARSER),
            'The items you want to update',
        )
    ).from_flask()

    auth.SiteSettingsPermissions().ensure_may_edit()

    for option in options.updates:
        # mypy bug: https://github.com/python/mypy/issues/9580
        edit_row = models.SiteSetting.set_option(
            option,  # type: ignore
            option.value,
        )
        models.db.session.add(edit_row)
    models.db.session.commit()

    return JSONResponse.make(site_settings.Opt.get_all_opts())
Пример #6
0
def get_data_source(
    ana_id: int,
    data_source_name: str,
) -> JSONResponse[models.BaseDataSource]:
    """Get a data source within a :class:`.models.AnalyticsWorkspace`.

    .. :quickref: Analytics; Get a data source of a workspace.

    .. warning::

        This route should be considered beta, its behavior and/or existence
        will change.

    :param int ana_id: The id of the workspace in which the datasource should
        be retrieved.
    :param string data_source_name: The name of the data source to retrieve.
    """
    workspace = helpers.get_or_404(
        models.AnalyticsWorkspace,
        ana_id,
        also_error=lambda a: not a.assignment.is_visible)
    auth.AnalyticsWorkspacePermissions(workspace).ensure_may_see()

    data_source = registry.analytics_data_sources[data_source_name](workspace)
    if not data_source.should_include():
        raise exceptions.APIException(
            'The given data source is not enabled for this workspace',
            (f'The data source {data_source_name} is not enabled for'
             f' worspace {ana_id}.'), exceptions.APICodes.OBJECT_NOT_FOUND,
            404)

    return JSONResponse.make(data_source)
Пример #7
0
def get_all_sso_providers() -> JSONResponse[t.Sequence[models.Saml2Provider]]:
    """Get all SSO Providers.

    .. :quickref: SSO Provider; Get all SSO Providers for this instance.
    """
    return JSONResponse.make(
        models.Saml2Provider.query.order_by(
            models.Saml2Provider.created_at.asc()).all())
Пример #8
0
def get_all_notifications() -> t.Union[ExtendedJSONResponse[NotificationsJSON],
                                       JSONResponse[HasUnreadNotifcationJSON],
                                       ]:
    """Get all notifications for the current user.

    .. :quickref: Notification; Get all notifications.

    :query boolean has_unread: If considered true a short digest will be send,
        i.e. a single object with one key ``has_unread`` with a boolean
        value. Please use this if you simply want to check if there are unread
        notifications.
    :returns: Either a :class:`.NotificationsJSON` or a
        `HasUnreadNotifcationJSON` based on the ``has_unread`` parameter.
    """
    notifications = db.session.query(Notification).join(
        Notification.comment_reply
    ).filter(
        ~models.CommentReply.deleted,
        Notification.receiver == current_user,
    ).order_by(
        Notification.read.asc(),
        Notification.created_at.desc(),
    ).options(
        contains_eager(Notification.comment_reply),
        defaultload(Notification.comment_reply).defer(
            models.CommentReply.last_edit
        ),
        defaultload(
            Notification.comment_reply,
        ).defaultload(
            models.CommentReply.comment_base,
        ).defaultload(
            models.CommentBase.file,
        ).selectinload(
            models.File.work,
        ),
    ).yield_per(_MAX_NOTIFICATION_AMOUNT)

    def can_see(noti: Notification) -> bool:
        return auth.NotificationPermissions(noti).ensure_may_see.as_bool()

    if request_arg_true('has_unread'):
        has_unread = any(
            map(can_see, notifications.filter(~Notification.read))
        )
        return JSONResponse.make({'has_unread': has_unread})

    return ExtendedJSONResponse.make(
        NotificationsJSON(
            notifications=[
                n for n in
                itertools.islice(notifications, _MAX_NOTIFICATION_AMOUNT)
                if can_see(n)
            ]
        ),
        use_extended=(models.CommentReply, Notification)
    )
Пример #9
0
def get_site_settings(
) -> t.Union[JSONResponse[site_settings.Opt.AllOptsAsJSON],
             JSONResponse[site_settings.Opt.FrontendOptsAsJSON],
             ]:
    """Get the settings for this CodeGrade instance.

    .. :quickref: Site settings; Get the site settings for this instance.
    """
    auth.SiteSettingsPermissions().ensure_may_see()
    return JSONResponse.make(site_settings.Opt.get_all_opts())
Пример #10
0
def create_sso_providers() -> JSONResponse[models.Saml2Provider]:
    """Register a new SSO Provider in this instance.

    .. :quickref: SSO Provider; Create a new SSO Providers in this instance.

    Users will be able to login using the registered provider.

    The request should contain two files. One named ``json`` containing the
    json data explained below and one named ``logo`` containing the backup
    logo.

    :>json metadata_url: The url where we can download the metadata for the IdP
        connected to this provider.
    :>json name: If no name can be found in the metadata this name will be
        displayed to users when choosing login methods.
    :>json description: If no description can be found in the metadata this
        description will be displayed to users when choosing login methods.

    :returns: The just created provider.
    """
    json_files = helpers.get_files_from_request(
        max_size=current_app.max_single_file_size, keys=['json'])
    data = helpers.ensure_json_dict(json.load(json_files[0]))

    with helpers.get_from_map_transaction(data) as [get, _]:
        metadata_url = get('metadata_url', str)
        name = get('name', str)
        description = get('description', str)

    logo = helpers.get_files_from_request(
        max_size=current_app.max_single_file_size, keys=['logo'])[0]

    prov = models.Saml2Provider(
        metadata_url=metadata_url,
        name=name,
        description=description,
        logo=logo,
    )
    db.session.add(prov)
    db.session.flush()
    try:
        prov.check_metadata_url()
    except:
        logger.error(
            'Error parsing given metadata url',
            exc_info=True,
            report_to_sentry=True,
        )
        raise exceptions.APIException(
            'Could not parse the metadata in the given metadata url',
            'The metadata could not be parsed',
            exceptions.APICodes.INVALID_PARAM, 400)
    db.session.commit()

    return JSONResponse.make(prov)
Пример #11
0
def do_oidc_login(
        lti_provider_id: t.Optional[str] = None) -> werkzeug.wrappers.Response:
    """Do an LTI 1.3 OIDC login.

    :param lti_provider_id: The id of the provider doing the launch, not
        required for LMSes that pass all required information.
    """
    req = helpers.maybe_unwrap_proxy(flask.request, flask.Request)
    if req.method == 'GET':
        target = req.args.get('target_link_uri')
    else:
        target = req.form.get('target_link_uri')
    assert target is not None

    try:
        provider = _maybe_get_provider(lti_provider_id)
        oidc = lti_v1_3.FlaskOIDCLogin.from_request(lti_provider=provider)
        red = oidc.get_redirect_object(target)
    except exceptions.APIException as exc:
        logger.info('Login request went wrong', exc_info=True)
        message = exc.message

        if exc.api_code == exceptions.APICodes.OBJECT_NOT_FOUND:
            message = (
                'This LMS was not found as a LTIProvider for CodeGrade, this'
                ' is probably caused by a wrong setup.')

        return _make_blob_and_redirect(
            {
                'type': 'exception',
                'exception_message': message,
                'original_exception': JSONResponse.dump_to_object(exc),
            },
            version=LTIVersion.v1_3,
            goto_latest_submission=False,
        )
    except pylti1p3.exception.OIDCException as exc:
        logger.info('Login request went wrong', exc_info=True)
        return _make_blob_and_redirect(
            {
                'type': 'exception',
                'exception_message': exc.args[0],
            },
            version=LTIVersion.v1_3,
            goto_latest_submission=False,
        )
    logger.info(
        'Redirecting after oidc',
        target=target,
        get_args=req.args,
        post_args=req.form,
        redirect=red,
    )
    return red.do_redirect()
Пример #12
0
 def _handle_rate_limit_exceeded(
         _: RateLimitExceeded) -> JSONResponse[errors.APIException]:
     return JSONResponse.make(
         errors.APIException(
             'Rate limit exceeded, slow down!',
             'Rate limit is exceeded',
             errors.APICodes.RATE_LIMIT_EXCEEDED,
             429,
         ),
         429,
     )
Пример #13
0
def _handle_lti_advantage_launch(
        lti_provider_id: t.Optional[str],
        goto_latest_sub: bool) -> t.Union[str, werkzeug.wrappers.Response]:
    app.config['SESSION_COOKIE_SAMESITE'] = 'None'

    try:
        provider = _maybe_get_provider(lti_provider_id)
        message_launch = lti_v1_3.FlaskMessageLaunch.from_request(
            lti_provider=provider, ).validate()
    except exceptions.APIException as exc:
        logger.info('An error occurred during the LTI launch', exc_info=True)
        return _make_blob_and_redirect(
            {
                'type': 'exception',
                'exception_message': exc.message,
                'original_exception': JSONResponse.dump_to_object(exc),
            },
            version=LTIVersion.v1_3,
            goto_latest_submission=False,
        )
    except pylti1p3.exception.LtiException as exc:
        logger.info('Incorrect LTI launch encountered', exc_info=True)
        return _make_blob_and_redirect(
            {
                'type': 'exception',
                'exception_message': exc.args[0],
            },
            version=LTIVersion.v1_3,
            goto_latest_submission=False,
        )

    plat_red_url = flask.request.args.get('platform_redirect_url')
    full_win_launch = flask.request.args.get('full_win_launch_requested')
    if full_win_launch == '1' and plat_red_url:
        return flask.redirect(plat_red_url)

    provider = message_launch.get_lti_provider()
    if (message_launch.is_deep_link_launch()
            and not provider.lms_capabilities.actual_deep_linking_required):
        deep_link = message_launch.get_deep_link()
        dp_resource = lti_v1_3.CGDeepLinkResource.make(message_launch)
        form = deep_link.output_response_form([dp_resource])
        return f'<!DOCTYPE html>\n<html><body>{form}</body></html>'

    return _make_blob_and_redirect(
        {
            'launch_data': message_launch.get_launch_data(),
            'lti_provider_id': message_launch.get_lti_provider().id,
            'request_args': dict(flask.request.args),
        },
        version=LTIVersion.v1_3,
        goto_latest_submission=goto_latest_sub,
    )
Пример #14
0
def get_user_preference(name: str) -> JSONResponse[t.Optional[bool]]:
    """Get a single UI preferences.

    .. :quickref: User Setting; Get a single UI preference.

    :query str token: The token with which you want to get the preferences,
        if not given the preferences are retrieved for the currently logged in
        user.
    :param string name: The preference name you want to get.
    :returns: The preferences for the user as described by the ``token``.
    """
    pref = rqa.EnumValue(models.UIPreferenceName).try_parse(name)
    user = _get_user()

    return JSONResponse.make(
        models.UIPreference.get_preference_for_user(user, pref)
    )
Пример #15
0
def get_notification_settings(
) -> JSONResponse[models.NotificationSettingJSON]:
    """Update preferences for notifications.

    .. :quickref: User Setting; Get the preferences for notifications.

    :query str token: The token with which you want to get the preferences,
        if not given the preferences are retrieved for the currently logged in
        user.
    :returns: The preferences for the user as described by the ``token``.
    """
    user = _get_user()

    return JSONResponse.make(
        models.NotificationsSetting.get_notification_setting_json_for_user(
            user,
        )
    )
Пример #16
0
def get_analytics(ana_id: int) -> JSONResponse[models.AnalyticsWorkspace]:
    """Get a :class:`.models.AnalyticsWorkspace`.

    .. :quickref: Analytics; Get a analytics workspace.

    .. warning::

        This route should be considered beta, its behavior and/or existence
        will change.

    :param int ana_id: The id of the workspace to get.
    """
    workspace = helpers.get_or_404(
        models.AnalyticsWorkspace,
        ana_id,
        also_error=lambda a: not a.assignment.is_visible)
    auth.AnalyticsWorkspacePermissions(workspace).ensure_may_see()
    return JSONResponse.make(workspace)
Пример #17
0
def get_task_result(task_result_id: uuid.UUID) -> JSONResponse[TaskResult]:
    """Get the state of a task result.

    .. :quickref: Task result; Get a single task result.


    .. note:

        To check if the task failed you should use the ``state`` attribute of
        the returned object as the status code of the response will still be
        200. It is 200 as we successfully fulfilled the request, which was
        getting the task result.

    :param task_result_id: The task result to get.
    :returns: The retrieved task result.
    """
    result = get_or_404(TaskResult, task_result_id)
    TaskResultPermissions(result).ensure_may_see()
    return JSONResponse.make(result)
Пример #18
0
def launch_lti() -> t.Any:
    """Do a LTI launch.
    """
    try:
        params = lti_v1_1.LTI.create_from_request(flask.request).launch_params
    except exceptions.APIException as exc:
        return _make_blob_and_redirect(
            {
                'type': 'exception',
                'exception_message': exc.message,
                'original_exception': JSONResponse.dump_to_object(exc),
            },
            version=LTIVersion.v1_1,
            goto_latest_submission=False,
        )

    return _make_blob_and_redirect(params,
                                   LTIVersion.v1_1,
                                   goto_latest_submission=False)
Пример #19
0
def get_login_link(
        login_link_id: uuid.UUID) -> JSONResponse[models.AssignmentLoginLink]:
    """Get a login link and the connected assignment.

    .. :quickref: Login link; Get a login link and its connected assignment.

    :param login_link_id: The id of the login link you want to get.

    :returns: The requested login link, which will also contain information
              about the connected assignment.
    """
    login_link = helpers.get_or_404(
        models.AssignmentLoginLink,
        login_link_id,
        also_error=lambda l:
        (not l.assignment.is_visible or not l.assignment.send_login_links))

    auth.set_current_user(login_link.user)
    return JSONResponse.make(login_link)
Пример #20
0
def get_user_preferences() -> JSONResponse[t.Mapping[str, t.Optional[bool]]]:
    """Get ui preferences.

    .. :quickref: User Setting; Get UI preferences.

    :query str token: The token with which you want to get the preferences,
        if not given the preferences are retrieved for the currently logged in
        user.
    :returns: The preferences for the user as described by the ``token``.
    """
    user = _get_user()

    return JSONResponse.make(
        {
            pref.name: value
            for pref, value in
            models.UIPreference.get_preferences_for_user(user).items()
        }
    )
Пример #21
0
def user_patch_handle_reset_password() -> JSONResponse[t.Mapping[str, str]]:
    """Handle the ``reset_password`` type for the PATCH login route.

    :returns: A response with a jsonified mapping between ``access_token`` and
        a token which can be used to login. This is only key available.
    """
    data = ensure_json_dict(
        request.get_json(),
        replace_log=lambda k, v: '<PASSWORD>' if 'password' in k else v
    )
    ensure_keys_in_dict(
        data, [('new_password', str), ('token', str), ('user_id', int)]
    )
    password = t.cast(str, data['new_password'])
    user_id = t.cast(int, data['user_id'])
    token = t.cast(str, data['token'])

    user = helpers.get_or_404(models.User, user_id)
    validate.ensure_valid_password(password, user=user)

    user.reset_password(token, password)
    db.session.commit()
    return JSONResponse.make({'access_token': user.make_access_token()})
Пример #22
0
def about(
) -> JSONResponse[t.Mapping[str, t.Union[str, object, t.Mapping[str, bool]]]]:
    """Get the version and features of the currently running instance.

    .. :quickref: About; Get the version and features.

    :>json string version: The version of the running instance.
    :>json object features: A mapping from string to a boolean for every
        feature indicating if the current instance has it enabled.

    :returns: The mapping as described above.
    """
    _no_val = object()
    status_code = 200

    features = {
        key.name: bool(value)
        for key, value in current_app.config['FEATURES'].items()
    }

    res = {
        'version': current_app.config['VERSION'],
        'commit': current_app.config['CUR_COMMIT'],
        'features': features,
    }

    if request.args.get('health', _no_val) == current_app.config['HEALTH_KEY']:
        try:
            database = len(
                models.Permission.get_all_permissions(
                    permissions.CoursePermission)) == len(CoursePermission)
        except:  # pylint: disable=bare-except
            logger.error('Database not working', exc_info=True)
            database = False

        uploads = check_dir(current_app.config['UPLOAD_DIR'], check_size=True)
        mirror_uploads = check_dir(current_app.config['MIRROR_UPLOAD_DIR'],
                                   check_size=True)
        temp_dir = check_dir(tempfile.gettempdir(), check_size=True)

        with helpers.BrokerSession() as ses:
            try:
                # Set a short timeout as the broker really shouldn't take
                # longer than 2 seconds to answer.
                ses.get('/api/v1/ping', timeout=2).raise_for_status()
            except RequestException:
                logger.error('Broker unavailable', exc_info=True)
                broker_ok = False
            else:
                broker_ok = True

        health_value = {
            'application': True,
            'database': database,
            'uploads': uploads,
            'broker': broker_ok,
            'mirror_uploads': mirror_uploads,
            'temp_dir': temp_dir,
        }
        res['health'] = health_value

        if not all(health_value.values()):
            status_code = 500

    return JSONResponse.make(res, status_code=status_code)
Пример #23
0
def deep_link_lti_assignment(
        deep_link_blob_id: uuid.UUID
) -> helpers.JSONResponse[t.Mapping[str, str]]:
    """Create a deeplink response for the given blob.

    :<json name: The name of the new assignment.
    :<json deadline: The deadline of the new assignment, formatted as an
        ISO8601 datetime string.
    :<json auth_token: The authentication token received after the initial LTI
        launch.

    :>json url: The url you should use to post the given ``jwt`` to.
    :>json jwt: The JWT that should be posted to the outputted ``url``.
    """
    blob = helpers.filter_single_or_404(
        models.BlobStorage,
        models.BlobStorage.id == deep_link_blob_id,
        with_for_update=True,
    )
    db.session.delete(blob)
    blob_json = blob.as_json()
    assert isinstance(blob_json, dict)
    assert blob_json['type'] == 'deep_link_settings'

    # We need a bit longer expiration here compared to other uses of the blob
    # storage in this module as users need to have the time to actually input
    # the needed data.
    if blob.age > timedelta(days=1):
        # Really delete the blob in this case too.
        db.session.commit()
        raise errors.APIException(
            'This deep linking session has expired, please reload',
            f'The deep linking session with blob {blob.id} has expired',
            errors.APICodes.OBJECT_EXPIRED, 400)

    provider = helpers.get_or_404(models.LTI1p3Provider,
                                  blob_json['lti_provider_id'])

    with helpers.get_from_request_transaction() as [get, _]:
        auth_token = get('auth_token', str)
        name = get('name', str)
        deadline_str = get('deadline', str)

    if auth_token != blob_json['auth_token']:
        raise exceptions.PermissionException(
            'You did not provide the correct token to deep link an assignment',
            f'The provided token {auth_token} is not correct',
            exceptions.APICodes.INCORRECT_PERMISSION, 403)

    deadline = parsers.parse_datetime(deadline_str)

    dp_resource = lti_v1_3.CGDeepLinkResource.make(
        lti_provider=provider, deadline=deadline).set_title(name)
    deep_link_settings = t.cast(t.Any, blob_json['deep_link_settings'])
    deep_link = DeepLink(
        lti_v1_3.CGRegistration(provider),
        t.cast(str, blob_json['deployment_id']),
        deep_link_settings,
    )
    db.session.commit()
    return JSONResponse.make({
        'url': deep_link_settings['deep_link_return_url'],
        'jwt': deep_link.get_response_jwt([dp_resource]),
    })
Пример #24
0
def about() -> JSONResponse[AboutAsJSON]:
    """Get information about this CodeGrade instance.

    .. :quickref: About; Get the version and features.
    """
    _no_val = object()
    status_code = 200

    settings = site_settings.Opt.get_frontend_opts()
    release_info = current_app.config['RELEASE_INFO']
    res: AboutAsJSON = {
        'version': release_info.get('version'),
        'commit': release_info['commit'],
        # We include the old features here to be backwards compatible.
        'features':
            {
                'AUTOMATIC_LTI_ROLE': settings['AUTOMATIC_LTI_ROLE_ENABLED'],
                'AUTO_TEST': settings['AUTO_TEST_ENABLED'],
                'BLACKBOARD_ZIP_UPLOAD':
                    settings['BLACKBOARD_ZIP_UPLOAD_ENABLED'],
                'COURSE_REGISTER': settings['COURSE_REGISTER_ENABLED'],
                'EMAIL_STUDENTS': settings['EMAIL_STUDENTS_ENABLED'],
                'GROUPS': settings['GROUPS_ENABLED'],
                'INCREMENTAL_RUBRIC_SUBMISSION':
                    settings['INCREMENTAL_RUBRIC_SUBMISSION_ENABLED'],
                'LINTERS': settings['LINTERS_ENABLED'],
                'LTI': settings['LTI_ENABLED'],
                'PEER_FEEDBACK': settings['PEER_FEEDBACK_ENABLED'],
                'REGISTER': settings['REGISTER_ENABLED'],
                'RENDER_HTML': settings['RENDER_HTML_ENABLED'],
                'RUBRICS': settings['RUBRICS_ENABLED'],
            },
        'settings': settings,
        'release': release_info,
    }

    if request.args.get('health', _no_val) == current_app.config['HEALTH_KEY']:
        try:
            database = len(
                models.Permission.get_all_permissions(
                    permissions.CoursePermission
                )
            ) == len(CoursePermission)
        except:  # pylint: disable=bare-except
            logger.error('Database not working', exc_info=True)
            database = False

        min_free = current_app.config['MIN_FREE_DISK_SPACE']
        uploads = current_app.file_storage.check_health(
            min_free_space=min_free
        )
        mirror_uploads = current_app.file_storage.check_health(
            min_free_space=min_free
        )

        with models.BrokerSetting.get_current().get_session(retries=2) as ses:
            try:
                # Set a short timeout as the broker really shouldn't take
                # longer than 2 seconds to answer.
                ses.get('/api/v1/ping', timeout=2).raise_for_status()
            except RequestException:
                logger.error('Broker unavailable', exc_info=True)
                broker_ok = False
            else:
                broker_ok = True

        health_value: HealthAsJSON = {
            'application': True,
            'database': database,
            'uploads': uploads,
            'broker': broker_ok,
            'mirror_uploads': mirror_uploads,
            'temp_dir': True,
        }

        res['health'] = health_value
        if not all(health_value.values()):
            status_code = 500

    return JSONResponse.make(res, status_code=status_code)