예제 #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 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())
예제 #3
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)
예제 #4
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())
예제 #5
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)
    )
예제 #6
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())
예제 #7
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)
예제 #8
0
파일: limiter.py 프로젝트: te5in/CodeGra.de
 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,
     )
예제 #9
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)
    )
예제 #10
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,
        )
    )
예제 #11
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)
예제 #12
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)
예제 #13
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)
예제 #14
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()
        }
    )
예제 #15
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()})
예제 #16
0
파일: lti.py 프로젝트: te5in/CodeGra.de
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]),
    })
예제 #17
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)
예제 #18
0
파일: about.py 프로젝트: te5in/CodeGra.de
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)